-- 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("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local DeckLib = {} local searchLib = require("util/SearchLib") -- places a card/deck at a position or merges into an existing deck ---@param obj tts__Object Object to move ---@param pos table New position for the object ---@param rot table New rotation for the object (optional) DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot) if obj == nil or pos == nil then return end -- search the new position for existing card/deck local searchResult = searchLib.atPosition(pos, "isCardOrDeck") -- get new position local newPos local offset = 0.5 if #searchResult == 1 then local bounds = searchResult[1].getBounds() newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) else newPos = Vector(pos) + Vector(0, offset, 0) end -- allow moving the objects smoothly out of the hand obj.use_hands = false if rot then obj.setRotationSmooth(rot, false, true) end obj.setPositionSmooth(newPos, false, true) -- continue if the card stops smooth moving Wait.condition( function() obj.use_hands = true -- this avoids a TTS bug that merges unrelated cards that are not resting if #searchResult == 1 and searchResult[1] ~= obj then -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put) pcall(function() searchResult[1].putObject(obj) end) end end, function() return not obj.isSmoothMoving() end, 3) end return DeckLib end end) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/MythosArea") end) __bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local PlayAreaApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") local function getPlayArea() return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") end local function getInvestigatorCounter() return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end -- Returns the current value of the investigator counter from the playmat ---@return number: Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getInvestigatorCounter().getVar("val") end -- Updates the current value of the investigator counter from the playmat ---@param count number Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) getInvestigatorCounter().call("updateVal", count) end -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' ---@param playerColor string Color of the player requesting the shift for messages PlayAreaApi.shiftContentsUp = function(playerColor) getPlayArea().call("shiftContentsUp", playerColor) end PlayAreaApi.shiftContentsDown = function(playerColor) getPlayArea().call("shiftContentsDown", playerColor) end PlayAreaApi.shiftContentsLeft = function(playerColor) getPlayArea().call("shiftContentsLeft", playerColor) end PlayAreaApi.shiftContentsRight = function(playerColor) getPlayArea().call("shiftContentsRight", playerColor) end ---@param state boolean This controls whether location connections should be drawn PlayAreaApi.setConnectionDrawState = function(state) getPlayArea().call("setConnectionDrawState", state) end ---@param color string Connection color to be used for location connections PlayAreaApi.setConnectionColor = function(color) getPlayArea().call("setConnectionColor", color) end -- Event to be called when the current scenario has changed ---@param scenarioName string Name of the new scenario PlayAreaApi.onScenarioChanged = function(scenarioName) getPlayArea().call("onScenarioChanged", scenarioName) end -- Sets this playmat's snap points to limit snapping to locations or not. -- If matchTypes is false, snap points will be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) getPlayArea().call("setLimitSnapsByType", matchCardTypes) end -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged -- cards before they're destroyed by entering the container PlayAreaApi.tryObjectEnterContainer = function(container, object) getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) end -- counts the VP on locations in the play area PlayAreaApi.countVP = function() return getPlayArea().call("countVP") end -- highlights all locations in the play area without metadata ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightMissingData = function(state) return getPlayArea().call("highlightMissingData", state) end -- highlights all locations in the play area with VP ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightCountedVP = function(state) return getPlayArea().call("countVP", state) end -- Checks if an object is in the play area (returns true or false) PlayAreaApi.isInPlayArea = function(object) return getPlayArea().call("isInPlayArea", object) end PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call PlayAreaApi.updateLocations = function(args) getPlayArea().call("updateLocations", args) end PlayAreaApi.getCustomDataHelper = function() return getPlayArea().getVar("customDataHelper") end return PlayAreaApi end end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local GUIDReferenceApi = {} local function getGuidHandler() return getObjectFromGUID("123456") end ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end -- returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end -- returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end -- sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object GUIDReferenceApi.editIndex = function(owner, type, guid) return getGuidHandler().call("editIndex", { owner = owner, type = type, guid = guid }) end return GUIDReferenceApi end end) __bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) do local CHAOS_TOKEN_NAMES = { ["Elder Sign"] = true, ["+1"] = true, ["0"] = true, ["-1"] = true, ["-2"] = true, ["-3"] = true, ["-4"] = true, ["-5"] = true, ["-6"] = true, ["-7"] = true, ["-8"] = true, ["Skull"] = true, ["Cultist"] = true, ["Tablet"] = true, ["Elder Thing"] = true, ["Auto-fail"] = true, ["Bless"] = true, ["Curse"] = true, ["Frost"] = true } local TokenChecker = {} -- returns true if the passed object is a chaos token (by name) TokenChecker.isChaosToken = function(obj) if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then return true else return false end end return TokenChecker end end) __bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local TokenSpawnTracker = {} local guidReferenceApi = require("core/GUIDReferenceApi") local function getSpawnTracker() return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker") end TokenSpawnTracker.hasSpawnedTokens = function(cardGuid) return getSpawnTracker().call("hasSpawnedTokens", cardGuid) end TokenSpawnTracker.markTokensSpawned = function(cardGuid) return getSpawnTracker().call("markTokensSpawned", cardGuid) end TokenSpawnTracker.resetTokensSpawned = function(cardGuid) return getSpawnTracker().call("resetTokensSpawned", cardGuid) end TokenSpawnTracker.resetAllAssetAndEvents = function() return getSpawnTracker().call("resetAllAssetAndEvents") end TokenSpawnTracker.resetAllLocations = function() return getSpawnTracker().call("resetAllLocations") end TokenSpawnTracker.resetAll = function() return getSpawnTracker().call("resetAll") end return TokenSpawnTracker end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, isTileOrToken = function(x) return x.type == "Tile" end } -- performs the actual search and returns a filtered list of object references ---@param pos tts__Vector Global position ---@param rot? tts__Vector Global rotation ---@param size table Size ---@param filter? string Name of the filter function ---@param direction? table Direction (positive is up) ---@param maxDistance? number Distance for the cast local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) local filterFunc if filter then filterFunc = filterFunctions[filter] end local searchResult = Physics.cast({ origin = pos, direction = direction or { 0, 1, 0 }, orientation = rot or { 0, 0, 0 }, type = 3, size = size, max_distance = maxDistance or 0 }) -- filtering the result local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then table.insert(objList, v.hit_object) end end return objList end -- searches the specified area SearchLib.inArea = function(pos, rot, size, filter) return returnSearchResult(pos, rot, size, filter) end -- searches the area on an object SearchLib.onObject = function(obj, filter) pos = obj.getPosition() size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) direction = { 0, -1, 0 } maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end return SearchLib end end) __bundle_register("core/MythosArea", function(require, _LOADED, __bundle_register, __bundle_modules) local deckLib = require("util/DeckLib") local guidReferenceApi = require("core/GUIDReferenceApi") local playAreaApi = require("core/PlayAreaApi") local searchLib = require("util/SearchLib") local tokenArrangerApi = require("accessories/TokenArrangerApi") local tokenChecker = require("core/token/TokenChecker") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") local ENCOUNTER_DECK_AREA = { upperLeft = { x = 0.9, z = 0.42 }, lowerRight = { x = 0.86, z = 0.38 }, } local ENCOUNTER_DISCARD_AREA = { upperLeft = { x = 1.62, z = 0.42 }, lowerRight = { x = 1.58, z = 0.38 }, } -- global position of encounter deck and discard pile local ENCOUNTER_DECK_POS = { x = -3.93, y = 1, z = 5.76 } local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1, z = 10.38 } local isReshuffling = false local collisionEnabled = false local currentScenario, useFrontData, tokenData local TRASH, DATA_HELPER function onLoad(saveState) if saveState ~= nil then local loadedState = JSON.decode(saveState) or {} currentScenario = loadedState.currentScenario or "" useFrontData = loadedState.useFrontData or true tokenData = loadedState.tokenData or {} end TRASH = guidReferenceApi.getObjectByOwnerAndType("Mythos", "Trash") DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper") Wait.time(function() collisionEnabled = true end, 0.1) end function onSave() return JSON.encode({ currentScenario = currentScenario, useFrontData = useFrontData, tokenData = tokenData }) end --------------------------------------------------------- -- collison and container event handling --------------------------------------------------------- -- TTS event handler. Handles scenario name event triggering and encounter card token resets. function onCollisionEnter(collisionInfo) if not collisionEnabled then return end local object = collisionInfo.collision_object -- early exit for better performance if object.type ~= "Card" then return end -- get scenario name and maybe fire followup event if object.getName() == "Scenario" then local description = object.getDescription() -- detect if a new scenario card is placed down if currentScenario ~= description then currentScenario = description fireScenarioChangedEvent() end local metadata = JSON.decode(object.getGMNotes()) or {} if not metadata["tokens"] then tokenData = {} return end -- detect orientation of scenario card (for difficulty) useFrontData = not object.is_face_down tokenData = metadata["tokens"][(useFrontData and "front" or "back")] fireTokenDataChangedEvent() end local localPos = self.positionToLocal(object.getPosition()) if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then Wait.frames(function() tokenSpawnTrackerApi.resetTokensSpawned(object.getGUID()) end, 1) removeTokensFromObject(object) end end -- TTS event handler. Handles scenario name event triggering function onCollisionExit(collisionInfo) local object = collisionInfo.collision_object -- reset token metadata if scenario reference card is removed if object.getName() == "Scenario" then tokenData = {} useFrontData = nil fireTokenDataChangedEvent() end end -- Listens for cards entering the encounter deck or encounter discard, and resets the spawn state -- for the cards when they do. function onObjectEnterContainer(container, object) local localPos = self.positionToLocal(container.getPosition()) if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then tokenSpawnTrackerApi.resetTokensSpawned(object.getGUID()) end end -- fires if the scenario title changes function fireScenarioChangedEvent() -- maybe show the title splash screen Wait.frames(function() Global.call('titleSplash', currentScenario) end, 20) -- set the scenario for the playarea (connections might be disabled) playAreaApi.onScenarioChanged(currentScenario) -- maybe update the playarea image local playAreaImageSelector = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayAreaImageSelector") playAreaImageSelector.call("maybeUpdatePlayAreaImage", currentScenario) end -- fires if the scenario title or the difficulty changes function fireTokenDataChangedEvent() local fullData = returnTokenData() tokenArrangerApi.onTokenDataChanged(fullData) end -- returns the chaos token metadata (if provided) function returnTokenData() return { tokenData = tokenData, currentScenario = currentScenario, useFrontData = useFrontData } end --------------------------------------------------------- -- encounter card drawing --------------------------------------------------------- -- gets the encounter deck (for internal functions and Api calls) function getEncounterDeck() local searchResult = searchLib.atPosition(ENCOUNTER_DECK_POS, "isCardOrDeck") if #searchResult > 0 then return searchResult[1] end end -- 'params' contains the position, rotation and a boolean to force a faceup draw function drawEncounterCard(params) local encounterDeck = getEncounterDeck() if encounterDeck then reshuffledAlready = false local card if encounterDeck.type == "Deck" then card = encounterDeck.takeObject() else card = encounterDeck end actualEncounterCardDraw(card, params) else -- nothing here, time to reshuffle if reshuffledAlready == true then reshuffledAlready = false return end reshuffleEncounterDeck() -- if there is no discard pile either, reshuffleEncounterDeck will give an error message already reshuffledAlready = true drawEncounterCard(params) end end function actualEncounterCardDraw(card, params) local faceUpRotation = 0 if not params.alwaysFaceUp then local metadata = JSON.decode(card.getGMNotes()) or {} if metadata.hidden or DATA_HELPER.call('checkHiddenCard', card.getName()) then faceUpRotation = 180 end end local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.625} local pos = params.mat.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET) local rotY = params.mat.getRotation().y deckLib.placeOrMergeIntoDeck(card, pos, { 0, rotY, faceUpRotation }) end function reshuffleEncounterDeck() -- flag to avoid multiple calls if isReshuffling then return end isReshuffling = true local encounterDeck = getEncounterDeck() local discardPile = searchLib.atPosition(ENCOUNTER_DISCARD_POSITION, "isCardOrDeck") if #discardPile > 0 then local discardDeck = discardPile[1] if not discardDeck.is_face_down then --flips discard pile discardDeck.setRotation({0, -90, 180}) end if encounterDeck == nil then discardDeck.setPosition(Vector(ENCOUNTER_DECK_POS) + Vector({0, 1, 0})) discardDeck.shuffle() else encounterDeck.putObject(discardDeck) encounterDeck.shuffle() end broadcastToAll("Shuffled encounter discard into deck.", "White") else broadcastToAll("Encounter discard pile is already empty.", "Red") end -- disable flag Wait.time(function() isReshuffling = false end, 1) end --------------------------------------------------------- -- helper functions --------------------------------------------------------- -- Simple method to check if the given point is in a specified area ---@param point tts__Vector Point to check, only x and z values are relevant ---@param bounds table Defined area to see if the point is within ---@return boolean: True if the point is in the area defined by bounds function inArea(point, bounds) return (point.x < bounds.upperLeft.x and point.x > bounds.lowerRight.x and point.z < bounds.upperLeft.z and point.z > bounds.lowerRight.z) end -- removes tokens from the provided card/deck function removeTokensFromObject(object) for _, obj in ipairs(searchLib.onObject(object)) do if obj.getGUID() ~= "4ee1f2" and -- table obj ~= self and obj.type ~= "Deck" and obj.type ~= "Card" and obj.memo ~= nil and obj.getLock() == false and not tokenChecker.isChaosToken(obj) then TRASH.putObject(obj) end end end end) __bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local TokenArrangerApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") -- local function to call the token arranger, if it is on the table ---@param functionName string Name of the function to cal ---@param argument? table Parameter to pass local function callIfExistent(functionName, argument) local tokenArranger = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenArranger") if tokenArranger ~= nil then tokenArranger.call(functionName, argument) end end -- updates the token modifiers with the provided data ---@param fullData table Contains the chaos token metadata TokenArrangerApi.onTokenDataChanged = function(fullData) callIfExistent("onTokenDataChanged", fullData) end -- deletes already laid out tokens TokenArrangerApi.deleteCopiedTokens = function() callIfExistent("deleteCopiedTokens") end -- updates the laid out tokens TokenArrangerApi.layout = function() Wait.time(function() callIfExistent("layout") end, 0.1) end return TokenArrangerApi end end) return __bundle_require("__root")