-- 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("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/VictoryDisplay") end) __bundle_register("core/VictoryDisplay", function(require, _LOADED, __bundle_register, __bundle_modules) local playAreaApi = require("core/PlayAreaApi") local tokenChecker = require("core/token/TokenChecker") local pendingCall = false local messageSent = {} local missingData = {} local countedVP = {} local highlightMissing = false local highlightCounted = false local TRASHCAN local TRASHCAN_GUID = "70b9f6" -- button creation when loading the game function onLoad() TRASHCAN = getObjectFromGUID(TRASHCAN_GUID) -- 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) self.createButton({ label = "!", click_function = "highlightMissingData", tooltip = "Enable highlighting of cards without metadata (VP on these is not counted).", function_owner = self, scale = { 0.15, 0.15, 0.15 }, color = { 1, 0, 0 }, width = 700, height = 800, font_size = 700, font_color = { 1, 1, 1 }, position = { x = 1.82, y = 0.06, z = -1.32 } }) -- index 4: highlighting button (counted VP) self.createButton({ label = "?", click_function = "highlightCountedVP", tooltip = "Enable highlighting of cards with VP.", function_owner = self, scale = { 0.15, 0.15, 0.15 }, color = { 0, 1, 0 }, width = 700, height = 800, font_size = 700, font_color = { 1, 1, 1 }, position = { x = 1.5, y = 0.06, z = -1.32 } }) -- 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 _, v in ipairs(searchOnObj(self)) do local obj = v.hit_object -- check metadata for VP if obj.tag == "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.tag == "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) -- check snap point states 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 fullSlots = {} local positions = {} for i, snap in ipairs(snaps) do positions[i] = self.positionToWorld(snap.position) local hits = checkSnapPointState(positions[i]) -- first hit is self, additional hits must be cards / decks if #hits > 1 then fullSlots[i] = true end end -- remove tokens from the card for _, v in ipairs(searchOnObj(card)) do local obj = v.hit_object -- don't touch decks / cards if obj.tag == "Deck" or obj.tag == "Card" then -- put chaos tokens back into bag elseif tokenChecker.isChaosToken(obj) then local chaosBag = Global.call("findChaosBag") chaosBag.putObject(obj) elseif obj.memo ~= nil and obj.getLock() == false then TRASHCAN.putObject(obj) end end -- place the card local name = card.getName() or "Unnamed card" for i = 1, 10 do if fullSlots[i] ~= true then local rot = { 0, 270, card.getRotation().z } card.setPositionSmooth(positions[i], false, true) card.setRotation(rot) broadcastToAll("Victory Display: " .. name .. " placed into slot " .. i .. ".", "Green") return end end broadcastToAll("Victory Display is full! " .. name .. " placed into slot 1.", "Orange") card.setPositionSmooth(positions[1], false, true) end --------------------------------------------------------- -- utility functions --------------------------------------------------------- -- searches on an object function searchOnObj(obj) return Physics.cast({ direction = { 0, 1, 0 }, max_distance = 0.5, type = 3, size = obj.getBounds().size, origin = obj.getPosition() }) end function checkSnapPointState(pos) return Physics.cast({ direction = { 0, 1, 0 }, max_distance = 0.1, type = 3, size = { 0.1, 0.1, 0.1 }, origin = pos }) end -- search a table for a value, return true if found (else returns false) function tableContains(table, value) for _, v in ipairs(table) do if v == value then return true end end return false end end) __bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local PlayAreaApi = { } local PLAY_AREA_GUID = "721ba2" local IMAGE_SWAPPER = "b7b45b" -- Returns the current value of the investigator counter from the playmat ---@return Integer. Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getObjectFromGUID(PLAY_AREA_GUID).call("getInvestigatorCount") end -- Updates the current value of the investigator counter from the playmat ---@param count Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) return getObjectFromGUID(PLAY_AREA_GUID).call("setInvestigatorCount", 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 Color of the player requesting the shift. Used solely to send an error --- message in the unlikely case that the scripting zone has been deleted PlayAreaApi.shiftContentsUp = function(playerColor) return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsUp", playerColor) end PlayAreaApi.shiftContentsDown = function(playerColor) return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsDown", playerColor) end PlayAreaApi.shiftContentsLeft = function(playerColor) return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsLeft", playerColor) end PlayAreaApi.shiftContentsRight = function(playerColor) return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsRight", playerColor) end -- Reset the play area's tracking of which cards have had tokens spawned. PlayAreaApi.resetSpawnedCards = function() return getObjectFromGUID(PLAY_AREA_GUID).call("resetSpawnedCards") end -- Event to be called when the current scenario has changed. ---@param scenarioName Name of the new scenario PlayAreaApi.onScenarioChanged = function(scenarioName) getObjectFromGUID(PLAY_AREA_GUID).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 matchTypes Boolean Whether snap points should only snap for the matching card types. PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) getObjectFromGUID(PLAY_AREA_GUID).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) getObjectFromGUID(PLAY_AREA_GUID).call("tryObjectEnterContainer", { container = container, object = object }) end -- counts the VP on locations in the play area PlayAreaApi.countVP = function() return getObjectFromGUID(PLAY_AREA_GUID).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 getObjectFromGUID(PLAY_AREA_GUID).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 getObjectFromGUID(PLAY_AREA_GUID).call("highlightCountedVP", state) end -- Checks if an object is in the play area (returns true or false) PlayAreaApi.isInPlayArea = function(object) return getObjectFromGUID(PLAY_AREA_GUID).call("isInPlayArea", object) end PlayAreaApi.getSurface = function() return getObjectFromGUID(PLAY_AREA_GUID).getCustomObject().image end PlayAreaApi.updateSurface = function(url) return getObjectFromGUID(IMAGE_SWAPPER).call("updateSurface", url) 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 CHAOS_TOKEN_NAMES[obj.getName()] then return true else return false end end return TokenChecker end end) return __bundle_require("__root")