-- Bundled by luabundle {"version":"1.6.0"} local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire) local loadingPlaceholder = {[{}] = true} local register local modules = {} local require local loaded = {} register = function(name, body) if not modules[name] then modules[name] = body end end require = function(name) local loadedModule = loaded[name] if loadedModule then if loadedModule == loadingPlaceholder then return nil end else if not modules[name] then if not superRequire then local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name) error('Tried to require ' .. identifier .. ', but no such module has been registered') else return superRequire(name) end end loaded[name] = loadingPlaceholder loadedModule = modules[name](require, loaded, register, modules) loaded[name] = loadedModule end return loadedModule end return require, loaded, register, modules end)(nil) __bundle_register("core/tour/TourStarter", function(require, _LOADED, __bundle_register, __bundle_modules) local tourManager = require("core/tour/TourManager") function onLoad() self.createButton({ click_function = "startTour", function_owner = self, position = { 1.27, 0.05, 0.035}, width = 500, height = 20, color = { 0, 0, 0, 0 }, -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size scale = { 1, 1, 0.82 }, tooltip = "Start the Tour", }) self.createButton({ click_function = "deleteStarter", function_owner = self, position = { 1.27, 0.05, 0.309}, width = 500, height = 20, color = { 0, 0, 0, 0 }, -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size scale = { 1, 1, 0.82 }, tooltip = "Delete this Panel", }) end function startTour(_, playerColor, _) tourManager.startTour(playerColor) end function deleteStarter(_, _, _) self.destruct() end end) __bundle_register("core/tour/TourManager", function(require, _LOADED, __bundle_register, __bundle_modules) do require("core/tour/TourScript") require("core/tour/TourCard") local TourManager = { } local internal = { } -- Base IDs for various tour card UI elements. Actual IDs will have _[playerColor] appended local CARD_ID = "tourCard" local LEFT_NARRATOR_ID = "tourNarratorImageLeft" local RIGHT_NARRATOR_ID = "tourNarratorImageRight" local BUBBLE_ID = "tourSpeechBubble" local TEXT_ID = "tourText" local NEXT_BUTTON_ID = "tourNext" local STOP_BUTTON_ID = "tourStop" -- Table centerpoint for the camera hook object. Camera handling is a bit erratic so it doesn't -- always land right where you think it's going to, but it's close local HOOK_CAMERA_HOME = { x = -30.2, y = 60, z = 0, } -- Default (0) position for the camera, as defined in the mod. If we don't recreate this position -- EXACTLY when exiting the tour then camera controls get weird local DEFAULT_CAMERA_POS = { position = { x = -22.265, y = -2.5, z = 5.2575}, pitch=64.343, yaw=90.333, distance=104.7} -- Global XML coordinates where we can present a card local SCREEN_POSITIONS = { center = "0 0 0", north = "0 300 0", east = "600 0 0", west = "-600 0 0", south = "0 -300 0", -- Northwest is only used by the Mandy card, move it a little right than standard so it's -- closer to the importer northwest = "-500 300 0", northeast = "600 300 0", southwest = "-600 -300 0", -- Used by the Diana and Wini cards referencing the bottom-right global controls, moved a little -- closer to them southeast = "730 -365 0" } -- Tracks the current state of the tours. Keyed by player color to keep each player's tour -- separate, will hold the camera hook and current card. local tourState = { } -- Kicks off the tour by initializing the card and camera hook. A callback on the hook creation -- will then show the first card. ---@param playerColor String Player color to start the tour for TourManager.startTour = function(playerColor) tourState[playerColor] = { currentCardIndex = 1 } -- Camera gets really screwy when we finalize if we don't start settled in ThirdPerson at the -- default position before attaching to the hook. Unfortunately there are no callbacks for when -- the movement is done, but the delay seems to handle it Player[playerColor].setCameraMode("ThirdPerson") Player[playerColor].lookAt(DEFAULT_CAMERA_POS) -- Initial camera rotation is painfully slow. White and Orange players are likely oriented -- correctly, but need a longer start delay for Green and Red local delay = 0.5 if playerColor ~= "White" and playerColor ~= "Orange" then delay = 2 broadcastToColor("Starting the tour, please wait...", playerColor) end Wait.time(function() internal.createTourCard(playerColor) -- XML update to add the new card takes a few frames to load, wait for it to finish then -- create the hook Wait.condition( function() internal.createCameraHook(playerColor) end, function() return not Global.UI.loading end ) end, delay) end -- Shows the next card in the tour script. This method is exposed (rather than being part of -- internal) because the XMLUI callbacks expect the method to be on the object directly. ---@param player Player object to show the next card for, provided by XMLUI callback function nextCard(player) internal.hideCard(player.color) Wait.time(function() tourState[player.color].currentCardIndex = tourState[player.color].currentCardIndex + 1 if tourState[player.color].currentCardIndex > #TOUR_SCRIPT then internal.finalizeTour(player.color) else internal.showCurrentCard(player.color) end end, 0.3) end -- Ends the tour and cleans up the camera. This method is exposed (rather than being part of -- internal) because the XMLUI callbacks expect the method to be on the object directly. ---@param player Player object to end the tour for, provided by XMLUI callback function stopTour(player) internal.hideCard(player.color) Wait.time(function() internal.finalizeTour(player.color) end, 0.3) end -- Updates the card UI for the script at the current index, moves the camera to the proper -- position, and shows the card. ---@param playerColor String Player color to show the current card for internal.showCurrentCard = function(playerColor) internal.updateCardDisplay(playerColor) local delay = 0 local cardIndex = tourState[playerColor].currentCardIndex local hook = getObjectFromGUID(tourState[playerColor].cameraHookGuid) if not TOUR_SCRIPT[cardIndex].skipCentering then hook.setPositionSmooth(HOOK_CAMERA_HOME, false, false) delay = delay + 0.5 end local lookPos if TOUR_SCRIPT[cardIndex].showObj ~= nil then local lookAtObj = getObjectFromGUID(TOUR_SCRIPT[cardIndex].showObj) lookPos = lookAtObj.getPosition() lookPos.y = TOUR_SCRIPT[cardIndex].distanceFromObj or 0 -- Since camera isn't directly above the hook, changing the Y affects the visual position of -- whatever object we're trying to look at. This is an approximation, but close enough to -- keep the object more centered lookPos.x = lookPos.x - lookPos.y / 2 elseif TOUR_SCRIPT[cardIndex].showPos ~= nil then lookPos = TOUR_SCRIPT[cardIndex].showPos end if lookPos ~= nil then Wait.time(function() hook.setPositionSmooth(lookPos, false, false) end, delay) delay = delay + 0.5 end Wait.time(function() Global.UI.show(internal.getUiId(CARD_ID, playerColor)) end, delay) end -- Hides the current card being shown to a player. This can be in preparation for showing the -- next card, or ending the tour. ---@param playerColor String Player color to hide the current card for internal.hideCard = function(playerColor) Global.UI.hide(internal.getUiId(CARD_ID, playerColor)) end -- Cleans up all the various resources associated with the tour, and (hopefully) resets the -- camera to the default position. Camera handling is erratic, the final card in the script -- should include instructions for the player to fix it. ---@param playerColor String Player color to clean up internal.finalizeTour = function(playerColor) local cameraHook = getObjectFromGUID(tourState[playerColor].cameraHookGuid) cameraHook.destruct() Player[playerColor].setCameraMode("ThirdPerson") tourState[playerColor] = nil Wait.frames(function() Player[playerColor].lookAt(DEFAULT_CAMERA_POS) end, 3) end -- Updates the card UI to show the appropriate card configuration. ---@param playerColor String Player color to update card for internal.updateCardDisplay = function(playerColor) local index = tourState[playerColor].currentCardIndex Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), "image", "Inv-" .. TOUR_SCRIPT[index].narrator) Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), "image", "Inv-" .. TOUR_SCRIPT[index].narrator) Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), "text", "\"" .. TOUR_SCRIPT[index].text .. "\"") local cardPos = TOUR_SCRIPT[index].position or "north" Global.UI.setAttribute(internal.getUiId(CARD_ID, playerColor), "position", SCREEN_POSITIONS[cardPos]) Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), "active", index < #TOUR_SCRIPT) -- Adjust images so the narrator is on the left or right, as defined by the card if TOUR_SCRIPT[index].speakerSide == "right" then Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), "active", false) Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), "active", true) Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), "rotation", "0 180 0") Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), "offsetXY", "-15 -15") Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), "offsetXY", "-35 -45") Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), "offsetXY", "5 -45") else Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), "active", true) Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), "active", false) Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), "rotation", "0 0 0") Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), "offsetXY", "15 -15") Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), "offsetXY", "-5 -45") Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), "offsetXY", "35 -45") end end -- Creates a small, transparent object which the camera will be attached to in order to move the -- user's view around the table. This should be called only at the beginning of the tour. Once -- creation is complete the user's camera will be attached to the hook and the first card will be -- shown. ---@param playerColor String Player color to create the hook for internal.createCameraHook = function(playerColor) local hookData = { Name = "BlockSquare", Transform = { posX = HOOK_CAMERA_HOME.x, posY = HOOK_CAMERA_HOME.y, posZ = HOOK_CAMERA_HOME.z, rotX = 0, rotY = 270.0, rotZ = 0, scaleX = 0.1, scaleY = 0.1, scaleZ = 0.1, }, ColorDiffuse = { r = 0, g = 0, b = 0, a = 0, }, Locked = true, GMNotes = playerColor } spawnObjectData({ data = hookData, callback_function = internal.onHookCreated }) end -- Callback for creation of the camera hook object. Will attach the camera and show the current -- (presumably first) card. ---@param hook Created object internal.onHookCreated = function(hook) local playerColor = hook.getGMNotes() tourState[playerColor].cameraHookGuid = hook.getGUID() Player[playerColor].attachCameraToObject({ object = hook, offset = { x = -20, y = 30, z = 0 } }) internal.showCurrentCard(playerColor) end -- Creates an XMLUI entry in Global for a player-specific tour card. Dynamically creating this -- is somewhat complex, but ensures we can properly handle any player color. ---@param playerColor String Player color to create the card for internal.createTourCard = function(playerColor) -- Make sure the card doesn't exist before we create a new one if Global.UI.getAttributes(internal.getUiId(CARD_ID, playerColor)) ~= nil then return end tourCardTemplate.attributes.id = internal.getUiId(CARD_ID, playerColor) tourCardTemplate.children[1].attributes.id = internal.getUiId(LEFT_NARRATOR_ID, playerColor) tourCardTemplate.children[2].attributes.id = internal.getUiId(RIGHT_NARRATOR_ID, playerColor) tourCardTemplate.children[3].attributes.id = internal.getUiId(BUBBLE_ID, playerColor) tourCardTemplate.children[4].attributes.id = internal.getUiId(TEXT_ID, playerColor) tourCardTemplate.children[5].attributes.id = internal.getUiId(NEXT_BUTTON_ID, playerColor) tourCardTemplate.children[5].attributes.onClick = self.getGUID().."/nextCard" tourCardTemplate.children[6].attributes.id = internal.getUiId(STOP_BUTTON_ID, playerColor) tourCardTemplate.children[6].attributes.onClick = self.getGUID().."/stopTour" internal.setDeepVisibility(tourCardTemplate, playerColor) local globalXml = Global.UI.getXmlTable() table.insert(globalXml, tourCardTemplate) Global.UI.setXmlTable(globalXml) end -- Panels don't cause their children to inherit their visibility value, so this recurses down the -- XML table to set all children to the same visibility. ---@param xmlUi Table. Lua table describing the XML ---@param playerColor String. String color of the player to make this visible for internal.setDeepVisibility = function(xmlUi, playerColor) xmlUi.attributes.visibility = "" .. playerColor if xmlUi.children ~= nil then for _, child in ipairs(xmlUi.children) do internal.setDeepVisibility(child, playerColor) end end end internal.getUiId = function(baseId, playerColor) return baseId .. "_" .. playerColor end return TourManager end end) __bundle_register("core/tour/TourCard", function(require, _LOADED, __bundle_register, __bundle_modules) -- Table definition for the tour card layout. This is functionally XMLUI in Lua form, but using -- this for dynamic creation ensures we can handle any player color without needing 10 -- near-duplicate definitions in Global.xml tourCardTemplate = { tag = "Panel", attributes = { id = "tourCard", height = 215, width = 330, rotation = "0 0 0", position = "0 300 30", showAnimation = "FadeIn", hideAnimation = "FadeOut", active=false, }, children = { { tag = "Image", attributes = { id = "tourNarratorImageLeft", height=120, width=80, rectAlignment="UpperLeft", offsetXY = "-80 0", -- Image will be set when the card is updated } }, { tag = "Image", attributes = { id = "tourNarratorImageRight", active = false, height=125, width=80, rectAlignment="UpperRight", offsetXY = "80 0" -- Image will be set when the card is updated } }, { tag = "Image", attributes = { id = "tourSpeechBubble", color = "#F5F5DC", height = 215, width = 330, rectAlignment = "MiddleCenter", image = "SpeechBubble", }, }, { tag = "Text", attributes = { id = "tourText", -- Everything on this is double-sized and scaled down to keep the text sharps height = 370, width = 520, scale = "0.5 0.5 1", rectAlignment = "UpperCenter", offsetXY = "15 -15", resizeTextForBestFit = true, resizeTextMinSize = 20, resizeTextMaxSize = 32, color = "#050505", alignment = "UpperLeft", horizontalOverflow = "wrap", } }, { tag = "Image", attributes = { id = "tourNext", height = 45, width = 45, rectAlignment = "LowerRight", offsetXY = "-5 -45", image = "NextArrow" }, }, { tag = "Image", attributes = { id = "tourStop", height = 45, width = 45, rectAlignment = "LowerLeft", offsetXY = "35 -45", image = "Exit" } }, } } end) __bundle_register("core/tour/TourScript", function(require, _LOADED, __bundle_register, __bundle_modules) -- Script for the SCED tour. Documentation and definitions to come. TOUR_SCRIPT = { { narrator = "Roland", text = "Despite my best efforts, looks like you found us. You may live to regret that. As long as you're here though we might as well show you around.\n\nUse the arrow to move forward, and if the horrors get to be too much you can quit whenever you like. Ready to get started?", position = "center" }, { narrator = "Darrell", text = "Cameras can be tricky things. Best you leave handling it to the professionals during the tour. Don't try to move the camera until the tour is complete.\n\nOnce we're done, remember you can use the 'p' key to switch back to third-person mode, and the spacebar to reset the position.", position = "center", speakerSide = "right", }, { narrator = "Daisy", text = "If you're new to the game, the library here has everything you'll need. A little research can go a long way, and looking into old newspapers for the weird and unusual can yield some surprisingly helpful information.\n\nI put a few right there that might prove enlightening.", showObj = "d99993", distanceFromObj = 20, position = "west", speakerSide = "right" }, { narrator = "Mandy", text = "To survive what's coming you'll need a deck. If it's safely hidden away on ArkhamDB you can load it here, and even find the newest version after an upgrade without changing the ID.\n\nNo need to publish all your decks, use 'Private' and you can see it. Just make sure to select 'Make your decks public' in ArkhamDB.", showObj = "a28140", distanceFromObj = -5, position = "northwest", skipCentering = true, }, { narrator = "Daniela", text = "I prefer the hands-on approach to building things, if you do too you can build a deck yourself.\n\nAll the cards you could ever need are here, laid out like a disassembled engine. Place the cards on the table, copy them for your deck, and you'll be ready for anything.", showObj = "2d30ee", distanceFromObj = -7, position = "south", speakerSide = "right" }, { narrator = "Finn", text = "Ready to face the unknown? We've smuggled shocking revelations and devious enemies from all over the world. Download the campaign you want to play, then Place it on the table to see the scenarios.\n\nJust remember - if it turns out to be too much for you, I was never here.", showObj = "aca04c", distanceFromObj = 20, position = "northwest", }, { narrator = "Diana", text = "These symbols on the bottom right are a repository of arcane knowledge, containing all the official content to download plus some deviously creative works from fans. One should beware those who seem too fond of the darkness, but you cannot deny the quality of their efforts.\n\nDon't see anything here? Only promoted players can access these.", position = "southeast", }, { narrator = "Winifred", text = "No good aviator would fly a plane she didn't know and hadn't tweaked a bit herself. The gear icon contains settings to customize your play experience, from alternate ways to track your clues to a variety of helpers to streamline the game.\n\nEverything here is optional, but who doesn't want to go as fast as they can? Just remember that all settings affect all players, so strap in and trust your pilot!", position = "southeast", }, { narrator = "Amina", text = "This is the Mythos area. Encounter cards, acts, and agenda will all be placed here while the large map below is where you will be exploring - be sure to set the number of investigators!\n\nYou can count doom on the agenda by clicking the large counter, and the smaller will automatically count doom tokens on the table. The chaos bag is in that book over on the right, and you can add or remove tokens from it whenever you need.", showPos = { x = -2.85, y = 0, z = 0.55 }, position = "north", speakerSide = "right" }, { narrator = "Gloria", text = "The evils that lurk in this world are out there, creeping ever closer. When they find you, this will easily draw a card from the encounter deck. The deck will even reshuffle itself when needed, for the enemies we face are unending.", showPos = { x = -35, y = -20, z = 28 }, position = "west", }, { narrator = "Jacqueline", text = "When the ire of fate finds you and the chaos looms, this large button will draw a chaos token. Click it again to return the token to the bag.\n\nWhether a vision of the future or a curse from the opponents we face, if you need additional tokens a right-click will draw more. I wish you luck, but have a vision of red tentacles reaching for you...", showPos = { x = -35, y = -20, z = 4.25 }, position = "north", skipCentering = true, speakerSide = "right" }, { narrator = "Preston", text = "I can afford to buy what I need, but for those less well-off we've provided an endless pool of tokens to track your game. Simply drag one out of the pools here.\n\nResources are my favorite of course, but damage and horror are as inevitable as taxes. I leave those to my bookkeeper though. Those tokens can work like counters, use the number keys to change the value.", showObj = "9fadf9", position = "north", skipCentering = true, speakerSide = "right" }, { narrator = "Norman", text = "That's the end of the tour, but there's much more to discover if you look in the right places. Some cards have helpers on the right-click menu, and every new version adds new content and functions.\n\nDon't be afraid to explore, and best of luck out there! We'll all need it...", position = "center", speakerSide = "right" }, } end) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/tour/TourStarter") end) return __bundle_require("__root")