-- 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("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local ChaosBagApi = {} -- respawns the chaos bag with a new state of tokens ---@param tokenList table List of chaos token ids ChaosBagApi.setChaosBagState = function(tokenList) Global.call("setChaosBagState", tokenList) end -- returns a Table List of chaos token ids in the current chaos bag -- requires copying the data into a new table because TTS is weird about handling table return values in Global ChaosBagApi.getChaosBagState = function() local chaosBagContentsCatcher = Global.call("getChaosBagState") local chaosBagContents = {} for _, v in ipairs(chaosBagContentsCatcher) do table.insert(chaosBagContents, v) end return chaosBagContents end -- checks scripting zone for chaos bag (also called by a lot of objects!) ChaosBagApi.findChaosBag = function() return Global.call("findChaosBag") end -- returns a table of object references to the tokens in play (does not include sealed tokens!) ChaosBagApi.getTokensInPlay = function() return Global.call("getChaosTokensinPlay") end -- returns all sealed tokens on cards to the chaos bag ---@param playerColor string Color of the player to show the broadcast to ChaosBagApi.releaseAllSealedTokens = function(playerColor) Global.call("releaseAllSealedTokens", playerColor) end -- returns all drawn tokens to the chaos bag ChaosBagApi.returnChaosTokens = function() Global.call("returnChaosTokens") end -- removes the specified chaos token from the chaos bag ---@param id string ID of the chaos token ChaosBagApi.removeChaosToken = function(id) Global.call("removeChaosToken", id) end -- returns a chaos token to the bag and calls all relevant functions ---@param token tts__Object Chaos token to return ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) end -- spawns the specified chaos token and puts it into the chaos bag ---@param id string ID of the chaos token ChaosBagApi.spawnChaosToken = function(id) Global.call("spawnChaosToken", id) end -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the -- contents of the bag should check this method before doing so. -- This method will broadcast a message to all players if the bag is being searched. ---@return any: True if the bag is manipulated, false if it should be blocked. ChaosBagApi.canTouchChaosTokens = function() return Global.call("canTouchChaosTokens") end -- draws a chaos token to a playermat ---@param mat tts__Object Playermat that triggered this ---@param drawAdditional boolean Controls whether additional tokens should be drawn ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token ---@return tts__Object: Object reference to the token that was drawn ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) return Global.call("drawChaosToken", { mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, takeParameters = takeParameters }) end -- returns a Table List of chaos token ids in the current chaos bag -- requires copying the data into a new table because TTS is weird about handling table return values in Global ChaosBagApi.getIdUrlMap = function() return Global.getTable("ID_URL_MAP") end return ChaosBagApi end end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local GUIDReferenceApi = {} local function getGuidHandler() return getObjectFromGUID("123456") end -- Returns the matching object ---@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 -- Returns the owner of an object or the object it's located on ---@param object tts__GameObject Object for this search ---@return string: Parent of the object or object it's located on GUIDReferenceApi.getOwnerOfObject = function(object) return getGuidHandler().call("getOwnerOfObject", object) end return GUIDReferenceApi end 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 playermat ---@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 playermat ---@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 playermat'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 -- Returns the current surface of the play area PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end -- Updates the surface of the play area PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end -- Returns a deep copy of the currently tracked locations PlayAreaApi.getTrackedLocations = function() local t = {} for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do t[k] = v end return t 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/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("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 below ---@param objOrTable tts__Object|table Object or table of objects to move ---@param pos table New position for the object ---@param rot? table New rotation for the object ---@param below? boolean Should the object be placed below an existing deck? DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) if objOrTable == nil or pos == nil then return end -- handle 'objOrTable' parameter local objects = {} if type(objOrTable) == "table" then objects = objOrTable else table.insert(objects, objOrTable) end -- search the new position for existing card/deck local searchResult = searchLib.atPosition(pos, "isCardOrDeck") local targetObj -- get new position local offset = 0.5 local newPos = Vector(pos) + Vector(0, offset, 0) if #searchResult == 1 then targetObj = searchResult[1] local bounds = targetObj.getBounds() if below then newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) else newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) end end -- process objects in reverse order for i = #objects, 1, -1 do local obj = objects[i] -- add a 0.1 delay for each object (for animation purposes) Wait.time(function() -- allow moving smoothly out of hand and temporarily lock it obj.setLock(true) obj.use_hands = false if rot then obj.setRotationSmooth(rot, false, true) end obj.setPositionSmooth(newPos, false, true) -- wait for object to finish movement (or 2 seconds) Wait.condition( function() -- revert toggles obj.setLock(false) obj.use_hands = true -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then targetObj = targetObj.putObject(obj) else targetObj = obj end end, -- check state of the object (make sure it's not moving) function() return obj.isDestroyed() or not obj.isSmoothMoving() end, 2) end, (#objects- i) * 0.1) end end return DeckLib end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { 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, isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" 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 }) -- filter the result for matching objects 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) local pos = obj.getPosition() local 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) local 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) local size = { 0.1, 2, 0.1 } local direction = { 0, -1, 0 } local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end return SearchLib end end) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/VictoryDisplay") end) __bundle_register("core/VictoryDisplay", function(require, _LOADED, __bundle_register, __bundle_modules) local chaosBagApi = require("chaosbag/ChaosBagApi") local deckLib = require("util/DeckLib") local guidReferenceApi = require("core/GUIDReferenceApi") local playAreaApi = require("core/PlayAreaApi") local searchLib = require("util/SearchLib") local tokenChecker = require("core/token/TokenChecker") local pendingCall = false local missingData = {} local countedVP = {} local highlightMissing = false local highlightCounted = false -- button creation when loading the game function onLoad() -- index 0: VP - "Display" local buttonParameters = {} buttonParameters.label = "0" buttonParameters.click_function = "none" buttonParameters.function_owner = self buttonParameters.scale = { 0.15, 0.15, 0.15 } buttonParameters.width = 0 buttonParameters.height = 0 buttonParameters.font_size = 600 buttonParameters.font_color = { 1, 1, 1 } buttonParameters.position = { x = -0.72, y = 0.06, z = -0.69 } self.createButton(buttonParameters) -- index 1: VP - "Play Area" buttonParameters.position.x = 0.65 self.createButton(buttonParameters) -- index 2: VP - "Total" buttonParameters.position.x = 1.69 self.createButton(buttonParameters) -- index 3: highlighting button (missing data) buttonParameters.label = "!" buttonParameters.click_function = "highlightMissingData" buttonParameters.tooltip = "Enable highlighting of cards without metadata (VP on these is not counted)." buttonParameters.color = { 1, 0, 0 } buttonParameters.width = 700 buttonParameters.height = 800 buttonParameters.font_size = 700 buttonParameters.position = { x = 1.82, y = 0.06, z = -1.32 } self.createButton(buttonParameters) -- index 4: highlighting button (counted VP) buttonParameters.label = "?" buttonParameters.click_function = "highlightCountedVP" buttonParameters.tooltip = "Enable highlighting of cards with VP." buttonParameters.color = { 0, 1, 0 } buttonParameters.position.x = 1.5 self.createButton(buttonParameters) -- update the display label once Wait.time(updateCount, 1) end --------------------------------------------------------- -- events with descriptions --------------------------------------------------------- -- dropping an object on the victory display function onCollisionEnter() startUpdate() end -- removing an object from the victory display function onCollisionExit() startUpdate() end -- picking a clue or location up function onObjectPickUp(_, obj) maybeUpdate(obj) end -- dropping a clue or location function onObjectDrop(_, obj) maybeUpdate(obj, 1) end -- flipping a clue/doom or location function onObjectRotate(obj, _, flip, _, _, oldFlip) if flip == oldFlip then return end maybeUpdate(obj, 1, true) end -- destroying a clue or location function onObjectDestroy(obj) maybeUpdate(obj) end --------------------------------------------------------- -- main functionality --------------------------------------------------------- function maybeUpdate(obj, delay, flipped) -- stop if there is already an update call running if pendingCall then return end -- stop if obj is nil (by e.g. dropping a clue onto another and making a stack) if obj == nil then return end -- only continue for clues / doom tokens or locations if obj.hasTag("Location") then elseif obj.memo == "clueDoom" then -- only continue if the clue side is up or a doom token is being flipped if obj.is_face_down == true and flipped ~= true then return end else return end -- only continue if the obj in in the play area if not playAreaApi.isInPlayArea(obj) then return end startUpdate(delay) end -- starts an update function startUpdate(delay) -- stop if there is already an update call running if pendingCall then return end pendingCall = true delay = tonumber(delay) or 0 Wait.time(updateCount, delay + 0.2) end -- counts the VP in the victory display and request the VP count from the play area function updateCount() missingData = {} countedVP = {} local victoryPoints = {} victoryPoints.display = 0 victoryPoints.playArea = playAreaApi.countVP() -- count cards in victory display for _, obj in ipairs(searchLib.onObject(self, "isCardOrDeck")) do -- check metadata for VP if obj.type == "Card" then local VP = getCardVP(obj, JSON.decode(obj.getGMNotes())) victoryPoints.display = victoryPoints.display + VP if VP > 0 then table.insert(countedVP, obj) end -- handling for stacked cards elseif obj.type == "Deck" then local VP = 0 for _, deepObj in ipairs(obj.getObjects()) do local deepVP = getCardVP(obj, JSON.decode(deepObj.gm_notes)) victoryPoints.display = victoryPoints.display + deepVP if deepVP > 0 then VP = VP + 1 end end if VP > 0 then table.insert(countedVP, obj) end end end -- update the buttons that are used as labels self.editButton({ index = 0, label = victoryPoints.display }) self.editButton({ index = 1, label = victoryPoints.playArea }) self.editButton({ index = 2, label = victoryPoints.display + victoryPoints.playArea }) -- allow new update calls pendingCall = false end -- gets the VP count from the notes function getCardVP(obj, notes) local cardVP if notes ~= nil then -- enemy, treachery etc. cardVP = tonumber(notes.victory) -- location if not cardVP then -- check the correct side of the location if not obj.is_face_down and notes.locationFront ~= nil then cardVP = tonumber(notes.locationFront.victory) elseif notes.locationBack ~= nil then cardVP = tonumber(notes.locationBack.victory) end end if (cardVP or 0) > 0 then table.insert(countedVP, obj) end else table.insert(missingData, obj) end return cardVP or 0 end -- toggles the highlight for objects with missing metadata function highlightMissingData() self.editButton({ index = 3, tooltip = (highlightMissing and "Enable" or "Disable") .. " highlighting of cards without metadata (VP on these is not counted)." }) for _, obj in pairs(missingData) do if obj ~= nil then if highlightMissing then obj.highlightOff("Red") else obj.highlightOn("Red") end end end playAreaApi.highlightMissingData(highlightMissing) highlightMissing = not highlightMissing end -- toggles the highlight for objects that were counted function highlightCountedVP() self.editButton({ index = 4, tooltip = (highlightCounted and "Enable" or "Disable") .. " highlighting of cards with VP." }) for _, obj in pairs(countedVP) do if obj ~= nil then if highlightCounted then obj.highlightOff("Green") else obj.highlightOn("Green") end end end playAreaApi.highlightCountedVP(highlightCounted) highlightCounted = not highlightCounted end -- places the provided card in the first empty spot function placeCard(card) local name = card.getName() or "Unnamed card" -- get sorted list of snap points to check slots local snaps = self.getSnapPoints() table.sort(snaps, function(a, b) return a.position.x > b.position.x end) table.sort(snaps, function(a, b) return a.position.z < b.position.z end) -- get first empty slot local emptyIndex, emptyPos for i, snap in ipairs(snaps) do local snapPos = self.positionToWorld(snap.position) local searchResult = searchLib.atPosition(snapPos, "isCardOrDeck") if #searchResult == 0 then emptyPos = snapPos emptyIndex = i break end end -- remove tokens from the card local trash = guidReferenceApi.getObjectByOwnerAndType("Mythos", "Trash") for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do if tokenChecker.isChaosToken(obj) then -- put chaos tokens back into bag local chaosBag = chaosBagApi.findChaosBag() chaosBag.putObject(obj) elseif obj.memo ~= nil and obj.getLock() == false then trash.putObject(obj) end end -- use the first snap position in case all slots are full local pos = emptyPos or self.positionToWorld(snaps[1].position) local rot = card.getRotation():setAt("x", 0):setAt("y", 270) deckLib.placeOrMergeIntoDeck(card, pos, rot) -- give a feedback message if emptyPos then broadcastToAll("Victory Display: " .. name .. " placed into slot " .. emptyIndex .. ".", "Green") else broadcastToAll("Victory Display is full! " .. name .. " placed into slot 1.", "Orange") end end end) return __bundle_require("__root")