2022-12-13 14:02:30 -05:00
-- 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)
2024-02-17 19:48:30 -05:00
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("playermat/Playmat")
end)
__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)
return 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)
return Global.call("releaseAllSealedTokens", playerColor)
end
-- returns all drawn tokens to the chaos bag
ChaosBagApi.returnChaosTokens = function()
return 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)
return 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
ChaosBagApi.returnChaosTokenToBag = function(token)
return Global.call("returnChaosTokenToBag", token)
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)
return 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 canTouch True if the bag is manipulated, false if it should be blocked.
ChaosBagApi.canTouchChaosTokens = function()
return Global.call("canTouchChaosTokens")
end
-- called by playermats (by the "Draw chaos token" button)
---@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
ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved)
return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved})
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)
2024-03-10 09:56:22 -04:00
__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)
2024-02-17 19:48:30 -05:00
__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local MythosAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
local function getMythosArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea")
end
---@return any: Table of chaos token metadata (if provided through scenario reference card)
MythosAreaApi.returnTokenData = function()
return getMythosArea().call("returnTokenData")
end
---@return any: Object reference to the encounter deck
MythosAreaApi.getEncounterDeck = function()
return getMythosArea().call("getEncounterDeck")
end
-- draw an encounter card for the requesting mat
---@param mat tts__Object Playermat that triggered this
---@param alwaysFaceUp boolean Whether the card should be drawn face-up
MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp)
getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp})
end
2024-03-10 09:56:22 -04:00
-- reshuffle the encounter deck
MythosAreaApi.reshuffleEncounterDeck = function()
getMythosArea().call("reshuffleEncounterDeck")
end
2024-02-17 19:48:30 -05:00
return MythosAreaApi
end
end)
2024-03-10 09:56:22 -04:00
__bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-17 19:48:30 -05:00
do
2024-03-10 09:56:22 -04:00
local NavigationOverlayApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
local function getNOHandler()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "NavigationOverlayHandler")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
-- copies the visibility for the Navigation overlay
---@param startColor string Color of the player to copy from
---@param targetColor string Color of the targeted player
NavigationOverlayApi.copyVisibility = function(startColor, targetColor)
getNOHandler().call("copyVisibility", {
startColor = startColor,
targetColor = targetColor
})
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
-- changes the Navigation Overlay view ("Full View" --> "Play Areas" --> "Closed" etc.)
---@param playerColor string Color of the player to update the visibility for
NavigationOverlayApi.cycleVisibility = function(playerColor)
getNOHandler().call("cycleVisibility", playerColor)
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
-- loads the specified camera for a player
---@param player tts__Player Player whose camera should be moved
---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to
NavigationOverlayApi.loadCamera = function(player, camera)
getNOHandler().call("loadCameraFromApi", {
player = player,
camera = camera
})
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
return NavigationOverlayApi
end
end)
__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local OptionPanelApi = {}
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
-- loads saved options
---@param options table Set a new state for the option table
OptionPanelApi.loadSettings = function(options)
return Global.call("loadSettings", options)
2024-01-06 21:32:29 -05:00
end
2024-03-10 09:56:22 -04:00
---@return any: Table of option panel state
OptionPanelApi.getOptions = function()
return Global.getTable("optionPanel")
end
return OptionPanelApi
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 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/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
2024-01-06 21:32:29 -05:00
end
end)
2024-01-06 21:32:07 -05:00
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
do
2024-01-06 21:32:29 -05:00
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-01-06 21:32:07 -05:00
local optionPanelApi = require("core/OptionPanelApi")
local playAreaApi = require("core/PlayAreaApi")
2024-02-04 10:51:51 -05:00
local searchLib = require("util/SearchLib")
2024-01-06 21:32:07 -05:00
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
local PLAYER_CARD_TOKEN_OFFSETS = {
[1] = {
Vector(0, 3, -0.2)
},
[2] = {
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[3] = {
Vector(0, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[4] = {
Vector(0.4, 3, -0.9),
Vector(-0.4, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[5] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[6] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2)
},
[7] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0, 3, 0.5)
},
[8] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(-0.35, 3, 0.5),
Vector(0.35, 3, 0.5)
},
[9] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5)
},
[10] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0, 3, 1.2)
},
[11] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(-0.35, 3, 1.2),
Vector(0.35, 3, 1.2)
},
[12] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0.7, 3, 1.2),
Vector(0, 3, 1.2),
Vector(-0.7, 3, 1.2)
}
}
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- stateIDs for the multi-stated resource tokens
local stateTable = {
["resource"] = 1,
2024-02-17 19:48:30 -05:00
["ammo"] = 2,
["bounty"] = 3,
["charge"] = 4,
2024-01-06 21:32:07 -05:00
["evidence"] = 5,
2024-02-17 19:48:30 -05:00
["secret"] = 6,
["supply"] = 7,
["offering"] = 8
2024-01-06 21:32:07 -05:00
}
2022-10-19 19:07:47 -04:00
2024-01-06 21:32:07 -05:00
-- Table of data extracted from the token source bag, keyed by the Memo on each token which
-- should match the token type keys ("resource", "clue", etc)
local tokenTemplates
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
local playerCardData
local locationData
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
local TokenManager = {}
local internal = {}
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Spawns tokens for the card. This function is built to just throw a card at it and let it do
-- the work once a card has hit an area where it might spawn tokens. It will check to see if
-- the card has already spawned, find appropriate data from either the uses metadata or the Data
-- Helper, and spawn the tokens.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to maybe spawn tokens for
---@param extraUses table A table of <use type>=<count> which will modify the number of tokens
2024-01-06 21:32:07 -05:00
--- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1
TokenManager.spawnForCard = function(card, extraUses)
if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then
return
end
local metadata = JSON.decode(card.getGMNotes())
if metadata ~= nil then
internal.spawnTokensFromUses(card, extraUses)
else
internal.spawnTokensFromDataHelper(card)
end
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- Spawns a set of tokens on the given card.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to spawn tokens on
---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource"
---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the
2024-01-06 21:32:07 -05:00
-- spawned state object rather than spawning multiple tokens
2024-02-17 19:48:30 -05:00
---@param shiftDown? number An offset for the z-value of this group of tokens
---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens
2024-01-06 21:32:07 -05:00
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)
local optionPanel = optionPanelApi.getOptions()
2022-10-19 19:07:47 -04:00
2024-01-06 21:32:07 -05:00
if tokenType == "damage" or tokenType == "horror" then
TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
elseif tokenType == "resource" and optionPanel["useResourceCounters"] == "enabled" then
TokenManager.spawnResourceCounterToken(card, tokenCount)
elseif tokenType == "resource" and optionPanel["useResourceCounters"] == "custom" and tokenCount == 0 then
TokenManager.spawnResourceCounterToken(card, tokenCount)
else
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)
end
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror
-- tokens.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to spawn tokens on
---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other
2024-01-06 21:32:07 -05:00
-- types should use spawnMultipleTokens()
2024-02-17 19:48:30 -05:00
---@param tokenValue number Value to set the damage/horror to
2024-01-06 21:32:07 -05:00
TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
if tokenValue < 1 or tokenValue > 50 then return end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end)
2020-12-06 09:42:32 -05:00
end
2024-01-06 21:32:07 -05:00
TokenManager.spawnResourceCounterToken = function(card, tokenCount)
local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
local rot = card.getRotation()
TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
spawned.call("updateVal", tokenCount)
end)
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawns a number of tokens.
2024-02-17 19:48:30 -05:00
---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue".
2024-01-06 21:32:07 -05:00
-- Other types should use spawnCounterToken()
2024-02-17 19:48:30 -05:00
---@param tokenCount number How many tokens to spawn
---@param shiftDown? number An offset for the z-value of this group of tokens
---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens
2024-01-06 21:32:07 -05:00
TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)
-- not checking the max at this point since clue offsets are calculated dynamically
if tokenCount < 1 then return end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
local offsets = {}
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
else
-- only up to 12 offset tables defined
if tokenCount > 12 then return end
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
-- Fix the y-position for the spawn, since positionToWorld considers rotation which can
-- have bad results for face up/down differences
offsets[i].y = card.getPosition().y + 0.15
end
end
2021-09-03 13:52:12 -04:00
2024-01-06 21:32:07 -05:00
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
2024-03-10 09:56:22 -04:00
offsets = {}
2024-01-06 21:32:29 -05:00
-- get a vector for the shifting (downwards local to the card)
local shiftDownVector = Vector(0, 0, shiftDown):rotateOver("y", card.getRotation().y)
2024-01-06 21:32:07 -05:00
for i, baseOffset in ipairs(baseOffsets) do
2024-01-06 21:32:29 -05:00
offsets[i] = baseOffset + shiftDownVector
2024-01-06 21:32:07 -05:00
end
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- handling for not provided subtype (for example when spawning from custom data helpers)
if subType == nil then
subType = ""
end
2024-03-10 09:56:22 -04:00
2024-01-06 21:32:07 -05:00
-- this is used to load the correct state for additional resource tokens (e.g. "Ammo")
local callback = nil
local stateID = stateTable[string.lower(subType)]
if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then
callback = function(spawned) spawned.setState(stateID) end
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)
end
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawns a single token at the given global position by copying it from the template bag.
2024-02-17 19:48:30 -05:00
---@param position tts__Vector Global position to spawn the token
---@param tokenType string type of token to spawn, valid values are "damage", "horror",
2024-01-06 21:32:07 -05:00
-- "resource", "doom", or "clue"
2024-02-17 19:48:30 -05:00
---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,
2024-01-06 21:32:07 -05:00
-- x and z will use the default rotation from the source bag
2024-02-17 19:48:30 -05:00
---@param callback? function A callback function triggered after the new token is spawned
2024-01-06 21:32:07 -05:00
TokenManager.spawnToken = function(position, tokenType, rotation, callback)
internal.initTokenTemplates()
local loadTokenType = tokenType
if tokenType == "clue" or tokenType == "doom" then
loadTokenType = "clueDoom"
end
if tokenTemplates[loadTokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local tokenTemplate = tokenTemplates[loadTokenType]
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
-- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
2024-03-10 09:56:22 -04:00
local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)
2024-01-06 21:32:07 -05:00
if rotation ~= nil then
rot.y = rotation.y
end
if tokenType == "doom" then
rot.z = 180
end
2021-09-03 13:52:12 -04:00
2024-01-06 21:32:07 -05:00
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
})
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Checks a card for metadata to maybe replenish it
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card object to be replenished
---@param uses table The already decoded metadata.uses (to avoid decoding again)
---@param mat tts__Object The playmat the card is placed on (for rotation and casting)
2024-01-06 21:32:07 -05:00
TokenManager.maybeReplenishCard = function(card, uses, mat)
-- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)
if uses[1].count and uses[1].replenish then
internal.replenishTokens(card, uses, mat)
end
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some
-- callers.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card object to reset the tokens for
2024-01-06 21:32:07 -05:00
TokenManager.resetTokensSpawned = function(card)
tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- Pushes new player card data into the local copy of the Data Helper player data.
2024-02-17 19:48:30 -05:00
---@param dataTable table Key/Value pairs following the DataHelper style
2024-01-06 21:32:07 -05:00
TokenManager.addPlayerCardData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
2023-08-27 21:09:46 -04:00
end
end
2024-01-06 21:32:07 -05:00
-- Pushes new location data into the local copy of the Data Helper location data.
2024-02-17 19:48:30 -05:00
---@param dataTable table Key/Value pairs following the DataHelper style
2024-01-06 21:32:07 -05:00
TokenManager.addLocationData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Checks to see if the given card has location data in the DataHelper
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to check for data
---@return boolean: True if this card has data in the helper, false otherwise
2024-01-06 21:32:07 -05:00
TokenManager.hasLocationData = function(card)
internal.initDataHelperData()
return internal.getLocationData(card) ~= nil
end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:07 -05:00
internal.initTokenTemplates = function()
if tokenTemplates ~= nil then
return
end
2024-01-06 21:32:29 -05:00
tokenTemplates = {}
local tokenSource = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSource")
2024-01-06 21:32:07 -05:00
for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do
local tokenName = tokenTemplate.Memo
tokenTemplates[tokenName] = tokenTemplate
2022-10-19 19:07:47 -04:00
end
2022-12-13 14:02:30 -05:00
end
2021-09-03 13:52:12 -04:00
2024-01-06 21:32:07 -05:00
-- Copies the data from the DataHelper. Will only happen once.
internal.initDataHelperData = function()
if playerCardData ~= nil then
return
end
2024-01-06 21:32:29 -05:00
local dataHelper = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper")
2024-01-06 21:32:07 -05:00
playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')
locationData = dataHelper.getTable('LOCATIONS_DATA')
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state
-- of the card for both locations and standard cards.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to maybe spawn tokens for
---@param extraUses table A table of <use type>=<count> which will modify the number of tokens
2024-01-06 21:32:07 -05:00
--- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1
internal.spawnTokensFromUses = function(card, extraUses)
local uses = internal.getUses(card)
if uses == nil then return end
2021-04-10 00:44:08 -04:00
2024-01-06 21:32:07 -05:00
-- go through tokens to spawn
2024-01-06 21:32:29 -05:00
local tokenCount
2024-01-06 21:32:07 -05:00
for i, useInfo in ipairs(uses) do
2024-01-06 21:32:29 -05:00
tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()
if extraUses ~= nil and extraUses[useInfo.type] ~= nil then
tokenCount = tokenCount + extraUses[useInfo.type]
2024-01-06 21:32:07 -05:00
end
-- Shift each spawned group after the first down so they don't pile on each other
2024-01-06 21:32:29 -05:00
TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)
2024-01-06 21:32:07 -05:00
end
2024-03-10 09:56:22 -04:00
2024-01-06 21:32:07 -05:00
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a card based on the data helper data. This will consider the face up/down state
-- of the card for both locations and standard cards.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to maybe spawn tokens for
2024-01-06 21:32:07 -05:00
internal.spawnTokensFromDataHelper = function(card)
internal.initDataHelperData()
local playerData = internal.getPlayerCardData(card)
if playerData ~= nil then
internal.spawnPlayerCardTokensFromDataHelper(card, playerData)
end
local locationData = internal.getLocationData(card)
if locationData ~= nil then
internal.spawnLocationTokensFromDataHelper(card, locationData)
end
end
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a player card using data retrieved from the Data Helper.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to maybe spawn tokens for
---@param playerData table Player card data structure retrieved from the DataHelper. Should be
2024-01-06 21:32:07 -05:00
-- the right data for this card.
internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)
2024-01-06 21:32:29 -05:00
local token = playerData.tokenType
local tokenCount = playerData.tokenCount
2024-01-06 21:32:07 -05:00
TokenManager.spawnTokenGroup(card, token, tokenCount)
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a location using data retrieved from the Data Helper.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to maybe spawn tokens for
---@param locationData table Location data structure retrieved from the DataHelper. Should be
2024-01-06 21:32:07 -05:00
-- the right data for this card.
internal.spawnLocationTokensFromDataHelper = function(card, locationData)
local clueCount = internal.getClueCountFromData(card, locationData)
if clueCount > 0 then
TokenManager.spawnTokenGroup(card, "clue", clueCount)
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
internal.getClueCountFromData = function(card, locationData)
-- Return the number of clues to spawn on this location
if locationData == nil then
error('attempted to get clue for unexpected object: ' .. card.getName())
return 0
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
if ((card.is_face_down and locationData.clueSide == 'back')
2024-03-10 09:56:22 -04:00
or (not card.is_face_down and locationData.clueSide == 'front')) then
2024-01-06 21:32:07 -05:00
if locationData.type == 'fixed' then
return locationData.value
elseif locationData.type == 'perPlayer' then
return locationData.value * playAreaApi.getInvestigatorCount()
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
error('unexpected location type: ' .. locationData.type)
end
return 0
end
-- Gets the right uses structure for this card, based on metadata and face up/down state
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card to pull the uses from
2024-01-06 21:32:07 -05:00
internal.getUses = function(card)
2024-03-10 09:56:22 -04:00
local metadata = JSON.decode(card.getGMNotes()) or {}
2024-01-06 21:32:07 -05:00
if metadata.type == "Location" then
if card.is_face_down and metadata.locationBack ~= nil then
return metadata.locationBack.uses
elseif not card.is_face_down and metadata.locationFront ~= nil then
return metadata.locationFront.uses
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
elseif not card.is_face_down then
return metadata.uses
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
return nil
2022-12-13 14:02:30 -05:00
end
2022-03-27 10:12:31 -04:00
2024-01-06 21:32:07 -05:00
-- Dynamically create positions for clues on a card.
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card the clues will be placed on
---@param count number How many clues?
---@return table: Array of global positions to spawn the clues at
2024-01-06 21:32:07 -05:00
internal.buildClueOffsets = function(card, count)
2024-03-10 09:56:22 -04:00
local cluePositions = {}
2024-01-06 21:32:07 -05:00
for i = 1, count do
local row = math.floor(1 + (i - 1) / 4)
local column = (i - 1) % 4
2024-03-10 09:56:22 -04:00
local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))
cluePos.y = cluePos.y + 0.05
table.insert(cluePositions, cluePos)
2024-01-06 21:32:07 -05:00
end
return cluePositions
end
2024-02-17 19:48:30 -05:00
---@param card tts__Object Card object to be replenished
---@param uses table The already decoded metadata.uses (to avoid decoding again)
---@param mat tts__Object The playmat the card is placed on (for rotation and casting)
2024-01-06 21:32:07 -05:00
internal.replenishTokens = function(card, uses, mat)
local cardPos = card.getPosition()
-- don't continue for cards on the deck (Norman) or in the discard pile
if mat.positionToLocal(cardPos).x < -1 then return end
-- get current amount of resource tokens on the card
local clickableResourceCounter = nil
local foundTokens = 0
2024-02-04 10:51:51 -05:00
for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
2024-01-06 21:32:07 -05:00
local memo = obj.getMemo()
if (stateTable[memo] or 0) > 0 then
2024-02-17 19:48:30 -05:00
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
2024-02-04 10:51:51 -05:00
end
2022-12-13 14:02:30 -05:00
2024-02-17 19:48:30 -05:00
-- this is the theoretical new amount of uses (to be checked below)
local newCount = foundTokens + uses[1].replenish
2022-12-13 14:02:30 -05:00
2024-02-17 19:48:30 -05:00
-- if there are already more uses than the replenish amount, keep them
if foundTokens > uses[1].count then
newCount = foundTokens
-- only replenish up until the replenish amount
elseif newCount > uses[1].count then
2024-03-10 09:56:22 -04:00
newCount = uses[1].count
end
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)
2024-01-06 21:32:29 -05:00
end
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
return TokenManager
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")
2024-01-06 21:32:29 -05:00
end
2024-01-06 21:32:07 -05:00
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getSpawnTracker().call("hasSpawnedTokens", cardGuid)
2024-01-06 21:32:29 -05:00
end
2024-01-06 21:32:07 -05:00
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getSpawnTracker().call("markTokensSpawned", cardGuid)
2024-01-06 21:32:29 -05:00
end
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getSpawnTracker().call("resetTokensSpawned", cardGuid)
2024-01-06 21:32:29 -05:00
end
2020-11-28 13:23:58 -05:00
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getSpawnTracker().call("resetAllAssetAndEvents")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.resetAllLocations = function()
return getSpawnTracker().call("resetAllLocations")
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
TokenSpawnTracker.resetAll = function()
return getSpawnTracker().call("resetAll")
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
return TokenSpawnTracker
2024-02-17 19:48:30 -05:00
end
2022-12-13 14:02:30 -05:00
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-04 10:51:51 -05:00
local chaosBagApi = require("chaosbag/ChaosBagApi")
local deckLib = require("util/DeckLib")
local guidReferenceApi = require("core/GUIDReferenceApi")
local mythosAreaApi = require("core/MythosAreaApi")
2023-08-27 21:09:46 -04:00
local navigationOverlayApi = require("core/NavigationOverlayApi")
2024-02-04 10:51:51 -05:00
local searchLib = require("util/SearchLib")
local tokenChecker = require("core/token/TokenChecker")
local tokenManager = require("core/token/TokenManager")
2022-12-13 14:02:30 -05:00
-- we use this to turn off collision handling until onLoad() is complete
2023-04-22 16:56:01 -04:00
local collisionEnabled = false
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- x-Values for discard buttons
local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91}
2022-10-19 19:07:47 -04:00
2023-08-27 21:09:46 -04:00
local SEARCH_AROUND_SELF_X_BUFFER = 8
2024-02-04 10:51:51 -05:00
-- defined areas for object searching
2023-01-29 19:31:52 -05:00
local MAIN_PLAY_AREA = {
upperLeft = {
x = 1.98,
2024-02-04 10:51:51 -05:00
z = 0.736
2023-01-29 19:31:52 -05:00
},
lowerRight = {
x = -0.79,
2024-02-04 10:51:51 -05:00
z = -0.39
2023-01-29 19:31:52 -05:00
}
}
local INVESTIGATOR_AREA = {
upperLeft = {
x = -1.084,
z = 0.06517
},
lowerRight = {
x = -1.258,
2024-02-04 10:51:51 -05:00
z = -0.0805
2023-01-29 19:31:52 -05:00
}
}
local THREAT_AREA = {
upperLeft = {
x = 1.53,
z = -0.34
},
lowerRight = {
x = -1.13,
2024-02-04 10:51:51 -05:00
z = -0.92
}
}
local DECK_DISCARD_AREA = {
upperLeft = {
x = -1.62,
z = 0.855
},
lowerRight = {
x = -2.02,
z = -0.245
},
center = {
x = -1.82,
y = 0.5,
z = 0.305
},
size = {
x = 0.4,
y = 3,
z = 1.1
2023-01-29 19:31:52 -05:00
}
}
2024-02-04 10:51:51 -05:00
-- local position of draw and discard pile
local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }
local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }
2020-11-28 13:23:58 -05:00
2024-02-04 10:51:51 -05:00
-- global position of encounter discard pile
local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38}
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
2024-01-06 21:32:07 -05:00
2024-02-04 10:51:51 -05:00
-- table of type-object reference pairs of all owned objects
local ownedObjects = {}
local matColor = self.getMemo()
2023-04-22 16:56:01 -04:00
2024-02-04 10:51:51 -05:00
-- variable to track the status of the "Show Draw Button" option
2023-01-29 19:31:52 -05:00
local isDrawButtonVisible = false
2023-04-22 16:56:01 -04:00
-- global variable to report "Dream-Enhancing Serum" status
isDES = false
2023-01-29 19:31:52 -05:00
function onSave()
return JSON.encode({
2023-04-22 16:56:01 -04:00
playerColor = playerColor,
2023-01-29 19:31:52 -05:00
activeInvestigatorId = activeInvestigatorId,
isDrawButtonVisible = isDrawButtonVisible
})
end
2022-10-19 19:07:47 -04:00
2024-02-04 10:51:51 -05:00
function onLoad(saveState)
self.interactable = false
2020-11-28 13:23:58 -05:00
2024-02-04 10:51:51 -05:00
-- get object references to owned objects
ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- button creation
2022-12-13 14:02:30 -05:00
for i = 1, 6 do
2024-02-04 10:51:51 -05:00
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i)
2020-11-28 13:23:58 -05:00
end
self.createButton({
2024-02-04 10:51:51 -05:00
click_function = "drawEncounterCard",
2020-11-28 13:23:58 -05:00
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {-1.84, 0, -0.65},
rotation = {0, 80, 0},
width = 265,
height = 190
2020-11-28 13:23:58 -05:00
})
self.createButton({
2023-04-22 16:56:01 -04:00
click_function = "drawChaosTokenButton",
2020-11-28 13:23:58 -05:00
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {1.85, 0, -0.74},
rotation = {0, -45, 0},
width = 135,
height = 135
2020-11-28 13:23:58 -05:00
})
self.createButton({
2022-12-13 14:02:30 -05:00
label = "Upkeep",
2021-09-03 13:52:12 -04:00
click_function = "doUpkeep",
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {1.84, 0.1, -0.44},
2021-09-03 13:52:12 -04:00
scale = {0.12, 0.12, 0.12},
width = 800,
height = 280,
font_size = 180
})
2023-04-22 16:56:01 -04:00
-- save state loading
2024-01-06 21:32:29 -05:00
local state = JSON.decode(saveState)
2021-02-13 12:12:29 -05:00
if state ~= nil then
2023-04-22 16:56:01 -04:00
playerColor = state.playerColor
2022-12-13 14:02:30 -05:00
activeInvestigatorId = state.activeInvestigatorId
2023-01-29 19:31:52 -05:00
isDrawButtonVisible = state.isDrawButtonVisible
2020-11-28 13:23:58 -05:00
end
2023-01-29 19:31:52 -05:00
showDrawButton(isDrawButtonVisible)
2023-04-22 16:56:01 -04:00
math.randomseed(os.time())
2024-02-17 19:48:30 -05:00
Wait.time(function() collisionEnabled = true end, 0.1)
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
2024-01-06 21:32:29 -05:00
-- searches an area and optionally filters the result
function searchArea(origin, size, filter)
2024-02-04 10:51:51 -05:00
return searchLib.inArea(origin, self.getRotation(), size, filter)
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:29 -05:00
-- finds all objects on the playmat and associated set aside zone.
function searchAroundSelf(filter)
2023-08-27 21:09:46 -04:00
local bounds = self.getBoundsNormalized()
-- Increase the width to cover the set aside zone
bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER
2024-01-06 21:32:29 -05:00
bounds.size.y = 1
2023-08-27 21:09:46 -04:00
-- Since the cast is centered on the position, shift left or right to keep the non-set aside edge
-- of the cast at the edge of the playmat
-- setAsideDirection accounts for the set aside zone being on the left or right, depending on the
-- table position of the playmat
local setAsideDirection = bounds.center.z > 0 and 1 or -1
local localCenter = self.positionToLocal(bounds.center)
2024-01-06 21:32:29 -05:00
localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
return searchArea(self.positionToWorld(localCenter), bounds.size, filter)
2023-08-27 21:09:46 -04:00
end
2024-01-06 21:32:29 -05:00
-- searches the area around the draw deck and discard pile
function searchDeckAndDiscardArea(filter)
local pos = self.positionToWorld(DECK_DISCARD_AREA.center)
local scale = self.getScale()
local size = {
x = DECK_DISCARD_AREA.size.x * scale.x,
y = DECK_DISCARD_AREA.size.y,
z = DECK_DISCARD_AREA.size.z * scale.z
}
return searchArea(pos, size, filter)
2023-08-27 21:09:46 -04:00
end
2023-04-22 16:56:01 -04:00
function doNotReady(card)
return card.getVar("do_not_ready") or false
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:29 -05:00
-- rounds a number to the specified amount of decimal places
2024-02-17 19:48:30 -05:00
---@param num number Initial value
---@param numDecimalPlaces number Amount of decimal places
2024-01-06 21:32:29 -05:00
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:29 -05:00
-- handles discarding for a list of objects
2024-02-17 19:48:30 -05:00
---@param objList table List of objects to discard
2024-01-06 21:32:29 -05:00
function discardListOfObjects(objList)
for _, obj in ipairs(objList) do
2024-02-04 10:51:51 -05:00
if obj.type == "Card" or obj.type == "Deck" then
2024-01-06 21:32:29 -05:00
if obj.hasTag("PlayerCard") then
2024-02-04 10:51:51 -05:00
deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())
2024-01-06 21:32:29 -05:00
else
2024-02-04 10:51:51 -05:00
deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, {x = 0, y = -90, z = 0})
2022-10-19 19:07:47 -04:00
end
2024-01-06 21:32:29 -05:00
-- put chaos tokens back into bag (e.g. Unrelenting)
elseif tokenChecker.isChaosToken(obj) then
2024-02-04 10:51:51 -05:00
chaosBagApi.returnChaosTokenToBag(obj)
2024-01-06 21:32:29 -05:00
-- don't touch locked objects (like the table etc.)
elseif not obj.getLock() then
ownedObjects.Trash.putObject(obj)
2022-10-19 19:07:47 -04:00
end
2022-12-13 14:02:30 -05:00
end
2021-11-16 23:41:43 -05:00
end
2021-09-03 13:52:12 -04:00
2024-01-06 21:32:29 -05:00
-- build a discard button to discard from searchPosition (number must be unique)
function makeDiscardButton(xValue, number)
2023-04-22 16:56:01 -04:00
local position = { xValue, 0.1, -0.94}
2022-12-13 14:02:30 -05:00
local searchPosition = {-position[1], position[2], position[3] + 0.32}
local handlerName = 'handler' .. number
2024-01-06 21:32:29 -05:00
self.setVar(handlerName, function()
local cardSizeSearch = {2, 1, 3.2}
local globalSearchPosition = self.positionToWorld(searchPosition)
local searchResult = searchArea(globalSearchPosition, cardSizeSearch)
return discardListOfObjects(searchResult)
end)
2022-12-13 14:02:30 -05:00
self.createButton({
label = "Discard",
click_function = handlerName,
function_owner = self,
2022-12-13 18:26:23 -05:00
position = {position[1], position[2], position[3] + 0.6},
2022-12-13 14:02:30 -05:00
scale = {0.12, 0.12, 0.12},
2023-01-29 19:31:52 -05:00
width = 900,
height = 350,
font_size = 220
2022-12-13 14:02:30 -05:00
})
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- calls the Upkeep function with correct parameter
function doUpkeepFromHotkey(color)
doUpkeep(_, color)
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
function doUpkeep(_, clickedByColor, isRightClick)
if isRightClick then
changeColor(clickedByColor)
2022-12-13 14:02:30 -05:00
return
end
2023-04-22 16:56:01 -04:00
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or clickedByColor
2022-12-13 14:02:30 -05:00
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
2023-04-22 16:56:01 -04:00
local forcedLearning = false
2023-08-27 21:09:46 -04:00
local rot = self.getRotation()
2024-01-06 21:32:29 -05:00
for _, obj in ipairs(searchAroundSelf()) do
2022-12-13 14:02:30 -05:00
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
2024-01-06 21:32:29 -05:00
elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
2022-12-13 14:02:30 -05:00
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
2023-01-29 19:31:52 -05:00
if not doNotReady(obj) then
2023-08-27 21:09:46 -04:00
local cardRotation = round(obj.getRotation().y, 0) - rot.y
local yRotDiff = 0
if cardRotation < 0 then
cardRotation = cardRotation + 360
end
-- rotate cards to the next multiple of 90° towards 0°
if cardRotation > 90 and cardRotation <= 180 then
yRotDiff = 90
elseif cardRotation < 270 and cardRotation > 180 then
yRotDiff = 270
end
-- set correct rotation for face-down cards
rot.z = obj.is_face_down and 180 or 0
obj.setRotation({rot.x, rot.y + yRotDiff, rot.z})
2022-12-13 14:02:30 -05:00
end
if cardMetadata.id == "08031" then
forcedLearning = true
end
if cardMetadata.uses ~= nil then
2023-08-27 21:09:46 -04:00
tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)
2022-12-13 14:02:30 -05:00
end
end
end
2022-03-27 10:12:31 -04:00
2023-01-29 19:31:52 -05:00
-- flip investigator mini-card and summoned servitor mini-card
-- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)
2024-02-17 19:48:30 -05:00
local miniId = string.match(activeInvestigatorId, ".....") .. "-m"
for _, obj in ipairs(getObjects()) do
if obj.type == "Card" and obj.is_face_down then
local notes = JSON.decode(obj.getGMNotes())
if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
obj.flip()
2022-12-13 14:02:30 -05:00
end
2022-03-27 10:12:31 -04:00
end
2022-12-13 14:02:30 -05:00
end
2022-03-27 10:12:31 -04:00
2023-04-22 16:56:01 -04:00
-- gain a resource (or two if playing Jenny Barnes)
2022-12-13 14:02:30 -05:00
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
2024-01-06 21:32:29 -05:00
updateCounter({type = "ResourceCounter", modifier = 2})
2022-12-13 14:02:30 -05:00
printToColor("Gaining 2 resources (Jenny)", messageColor)
2023-04-22 16:56:01 -04:00
else
2024-01-06 21:32:29 -05:00
updateCounter({type = "ResourceCounter", modifier = 1})
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
-- draw a card (with handling for Patrice and Forced Learning)
if activeInvestigatorId == "06005" then
if forcedLearning then
printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
else
2023-04-22 16:56:01 -04:00
local handSize = #Player[playerColor].getHandObjects()
2022-12-13 14:02:30 -05:00
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
2023-08-27 21:09:46 -04:00
elseif activeInvestigatorId == "89001" then
printToColor("Drawing 2 cards (Subject 5U-21)", messageColor)
drawCardsWithReshuffle(2)
2022-12-13 14:02:30 -05:00
else
drawCardsWithReshuffle(1)
end
2020-11-28 13:23:58 -05:00
end
2023-01-29 19:31:52 -05:00
-- function for "draw 1 button" (that can be added via option panel)
2022-12-13 14:02:30 -05:00
function doDrawOne(_, color)
2023-04-22 16:56:01 -04:00
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or color
2022-12-13 14:02:30 -05:00
drawCardsWithReshuffle(1)
2021-11-16 23:41:43 -05:00
end
2022-12-13 14:02:30 -05:00
-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
2024-01-06 21:32:29 -05:00
local deckAreaObjects = getDeckAreaObjects()
2022-12-13 14:02:30 -05:00
-- Norman Withers handling
2024-01-06 21:32:29 -05:00
local harbinger = false
if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then
harbinger = true
elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then
local cards = deckAreaObjects.draw.getObjects()
if cards[#cards].name == "The Harbinger" then
harbinger = true
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:29 -05:00
end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:29 -05:00
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
2024-01-06 21:32:07 -05:00
return
end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:29 -05:00
local topCardDetected = false
if deckAreaObjects.topCard ~= nil then
deckAreaObjects.topCard.deal(1, playerColor)
topCardDetected = true
numCards = numCards - 1
if numCards == 0 then
flipTopCardFromDeck()
return
2022-12-13 14:02:30 -05:00
end
end
2021-11-16 23:41:43 -05:00
2022-12-13 14:02:30 -05:00
local deckSize = 1
2024-01-06 21:32:29 -05:00
if deckAreaObjects.draw == nil then
2022-12-13 14:02:30 -05:00
deckSize = 0
2024-01-06 21:32:29 -05:00
elseif deckAreaObjects.draw.type == "Deck" then
deckSize = #deckAreaObjects.draw.getObjects()
2022-12-13 14:02:30 -05:00
end
if deckSize >= numCards then
drawCards(numCards)
2024-01-06 21:32:29 -05:00
-- flip top card again for Norman
if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
flipTopCardFromDeck()
end
else
drawCards(deckSize)
if deckAreaObjects.discard ~= nil then
shuffleDiscardIntoDeck()
Wait.time(function()
drawCards(numCards - deckSize)
-- flip top card again for Norman
if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
flipTopCardFromDeck()
end
end, 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:29 -05:00
-- get the draw deck and discard pile objects and returns the references
function getDeckAreaObjects()
local deckAreaObjects = {}
2024-02-04 10:51:51 -05:00
for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do
2024-01-06 21:32:29 -05:00
if self.positionToLocal(object.getPosition()).z > 0.5 then
deckAreaObjects.discard = object
-- Norman Withers handling
elseif object.type == "Card" and not object.is_face_down then
deckAreaObjects.topCard = object
else
deckAreaObjects.draw = object
2022-12-13 14:02:30 -05:00
end
end
2024-01-06 21:32:29 -05:00
return deckAreaObjects
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
function drawCards(numCards)
2024-01-06 21:32:29 -05:00
local deckAreaObjects = getDeckAreaObjects()
if deckAreaObjects.draw then
deckAreaObjects.draw.deal(numCards, playerColor)
end
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
function shuffleDiscardIntoDeck()
2024-01-06 21:32:29 -05:00
local deckAreaObjects = getDeckAreaObjects()
if not deckAreaObjects.discard.is_face_down then
deckAreaObjects.discard.flip()
end
deckAreaObjects.discard.shuffle()
deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- utility function for Norman Withers to flip the top card to the revealed side
function flipTopCardFromDeck()
Wait.time(function()
local deckAreaObjects = getDeckAreaObjects()
if deckAreaObjects.topCard then
elseif deckAreaObjects.draw then
if deckAreaObjects.draw.type == "Card" then
deckAreaObjects.draw.flip()
else
-- get bounds to know the height of the deck
local bounds = deckAreaObjects.draw.getBounds()
local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)
deckAreaObjects.draw.takeObject({ position = pos, flip = true })
end
end
end, 0.1)
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
-- discard a random non-hidden card from hand
function doDiscardOne()
local hand = Player[playerColor].getHandObjects()
if #hand == 0 then
broadcastToAll("Cannot discard from empty hand!", "Red")
else
local choices = {}
for i = 1, #hand do
local notes = JSON.decode(hand[i].getGMNotes())
if notes ~= nil then
if notes.hidden ~= true then
table.insert(choices, i)
end
else
table.insert(choices, i)
end
end
if #choices == 0 then
broadcastToAll("Hidden cards can't be randomly discarded.", "Orange")
return
end
-- get a random non-hidden card (from the "choices" table)
local num = math.random(1, #choices)
2024-02-04 10:51:51 -05:00
deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())
2024-02-17 19:48:30 -05:00
local playerName = Player[playerColor].steam_name or playerColor
broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White")
2023-04-22 16:56:01 -04:00
end
end
---------------------------------------------------------
-- color related functions
---------------------------------------------------------
-- changes the player color
function changeColor(clickedByColor)
local colorList = {
"White",
"Brown",
"Red",
"Orange",
"Yellow",
"Green",
"Teal",
"Blue",
"Purple",
"Pink"
}
-- remove existing colors from the list of choices
for _, existingColor in ipairs(Player.getAvailableColors()) do
for i, newColor in ipairs(colorList) do
if existingColor == newColor then
table.remove(colorList, i)
end
end
end
-- show the option dialog for color selection to the player that triggered this
Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color)
-- update the color of the hand zone
2024-01-06 21:32:29 -05:00
local handZone = ownedObjects.HandZone
2023-04-22 16:56:01 -04:00
handZone.setValue(color)
-- if the seated player clicked this, reseat him to the new color
if clickedByColor == playerColor then
2023-08-27 21:09:46 -04:00
navigationOverlayApi.copyVisibility(playerColor, color)
2023-04-22 16:56:01 -04:00
Player[playerColor].changeColor(color)
end
-- update the internal variable
playerColor = color
end)
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
-- Finds all customizable cards in this play area and updates their metadata based on the selections
-- on the matching upgrade sheet.
-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be
-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the
-- number of customizable cards in play.
function syncAllCustomizableCards()
2024-02-04 10:51:51 -05:00
for _, card in ipairs(searchAroundSelf("isCard")) do
2023-08-27 21:09:46 -04:00
syncCustomizableMetadata(card)
2023-01-29 19:31:52 -05:00
end
2020-11-28 13:23:58 -05:00
end
2023-01-29 19:31:52 -05:00
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
2023-08-27 21:09:46 -04:00
if cardMetadata == nil or cardMetadata.customizations == nil then
return
end
2024-02-04 10:51:51 -05:00
for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do
2023-08-27 21:09:46 -04:00
local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { }
if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then
for i, customization in ipairs(cardMetadata.customizations) do
if customization.replaces ~= nil and customization.replaces.uses ~= nil then
-- Allowed use of call(), no APIs for individual cards
if upgradeSheet.call("isUpgradeActive", i) then
cardMetadata.uses = customization.replaces.uses
card.setGMNotes(JSON.encode(cardMetadata))
else
-- TODO: Get the original metadata to restore it... maybe. This should only be
-- necessary in the very unlikely case that a user un-checks a previously-full upgrade
-- row while the card is in play. It will be much easier once the AllPlayerCardsApi is
-- in place, so defer until it is
2022-12-13 14:02:30 -05:00
end
end
end
2022-10-19 19:07:47 -04:00
end
2020-11-28 13:23:58 -05:00
end
end
2023-01-29 19:31:52 -05:00
function spawnTokensFor(object)
local extraUses = { }
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2023-01-29 19:31:52 -05:00
tokenManager.spawnForCard(object, extraUses)
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:29 -05:00
function onCollisionEnter(collisionInfo)
local object = collisionInfo.collision_object
2023-04-22 16:56:01 -04:00
-- only continue if loading is completed
if not collisionEnabled then return end
2022-12-13 14:02:30 -05:00
-- only continue for cards
2024-02-04 10:51:51 -05:00
if object.type ~= "Card" then return end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- detect if "Dream-Enhancing Serum" is placed
if object.getName() == "Dream-Enhancing Serum" then isDES = true end
2023-01-29 19:31:52 -05:00
2022-12-13 14:02:30 -05:00
maybeUpdateActiveInvestigator(object)
2023-01-29 19:31:52 -05:00
syncCustomizableMetadata(object)
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:29 -05:00
local localCardPos = self.positionToLocal(object.getPosition())
if inArea(localCardPos, DECK_DISCARD_AREA) then
2023-01-29 19:31:52 -05:00
tokenManager.resetTokensSpawned(object)
2023-04-22 16:56:01 -04:00
removeTokensFromObject(object)
2023-01-29 19:31:52 -05:00
elseif shouldSpawnTokens(object) then
2020-11-28 13:23:58 -05:00
spawnTokensFor(object)
end
end
2023-04-22 16:56:01 -04:00
-- detect if "Dream-Enhancing Serum" is removed
2024-01-06 21:32:29 -05:00
function onCollisionExit(collisionInfo)
if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end
2023-04-22 16:56:01 -04:00
end
-- checks if tokens should be spawned for the provided card
2023-01-29 19:31:52 -05:00
function shouldSpawnTokens(card)
if card.is_face_down then
return false
end
2023-08-27 21:09:46 -04:00
2023-01-29 19:31:52 -05:00
local localCardPos = self.positionToLocal(card.getPosition())
local metadata = JSON.decode(card.getGMNotes())
2023-04-22 16:56:01 -04:00
2023-01-29 19:31:52 -05:00
-- If no metadata we don't know the type, so only spawn in the main area
if metadata == nil then
return inArea(localCardPos, MAIN_PLAY_AREA)
end
2023-04-22 16:56:01 -04:00
-- Spawn tokens for assets and events on the main area
2023-01-29 19:31:52 -05:00
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
2023-04-22 16:56:01 -04:00
-- Spawn tokens for all encounter types in the threat area
2023-01-29 19:31:52 -05:00
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
2024-01-06 21:32:29 -05:00
or metadata.type == "Enemy"
or metadata.weakness) then
return true
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
return false
2021-01-02 22:49:38 -05:00
end
2024-01-06 21:32:29 -05:00
function onObjectEnterContainer(container, object)
2024-02-04 10:51:51 -05:00
if object.type ~= "Card" then return end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
local localCardPos = self.positionToLocal(object.getPosition())
if inArea(localCardPos, DECK_DISCARD_AREA) then
tokenManager.resetTokensSpawned(object)
removeTokensFromObject(object)
2023-01-29 19:31:52 -05:00
end
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
-- removes tokens from the provided card/deck
function removeTokensFromObject(object)
2024-02-04 10:51:51 -05:00
if object.hasTag("CardThatSeals") then
local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content)
if func ~= nil then
object.call("resetSealedTokens")
end
end
for _, obj in ipairs(searchLib.onObject(object)) do
if tokenChecker.isChaosToken(obj) then
chaosBagApi.returnChaosTokenToBag(obj)
elseif obj.getGUID() ~= "4ee1f2" and -- table
2024-01-06 21:32:29 -05:00
obj ~= self and
obj.type ~= "Deck" and
obj.type ~= "Card" and
obj.memo ~= nil and
obj.getLock() == false and
2024-02-04 10:51:51 -05:00
obj.getDescription() ~= "Action Token" then
2024-01-06 21:32:29 -05:00
ownedObjects.Trash.putObject(obj)
end
2023-01-29 19:31:52 -05:00
end
2023-08-27 21:09:46 -04:00
end
2024-01-06 21:32:29 -05:00
---------------------------------------------------------
-- investigator ID grabbing and skill tracker
---------------------------------------------------------
function maybeUpdateActiveInvestigator(card)
if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end
local notes = JSON.decode(card.getGMNotes())
local class
if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then
if notes.id == activeInvestigatorId then return end
class = notes.class
activeInvestigatorId = notes.id
ownedObjects.InvestigatorSkillTracker.call("updateStats", {
notes.willpowerIcons,
notes.intellectIcons,
notes.combatIcons,
notes.agilityIcons
2023-08-27 21:09:46 -04:00
})
2024-01-06 21:32:29 -05:00
elseif activeInvestigatorId ~= "00000" then
class = "Neutral"
activeInvestigatorId = "00000"
ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1})
else
return
2023-08-27 21:09:46 -04:00
end
2024-01-06 21:32:29 -05:00
-- change state of action tokens
local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1})
local smallToken = nil
local STATE_TABLE = {
["Guardian"] = 1,
["Seeker"] = 2,
["Rogue"] = 3,
["Mystic"] = 4,
["Survivor"] = 5,
["Neutral"] = 6
}
for _, obj in ipairs(search) do
if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
if obj.getScale().x < 0.4 then
smallToken = obj
else
setObjectState(obj, STATE_TABLE[class])
end
end
2023-08-27 21:09:46 -04:00
end
2024-01-06 21:32:29 -05:00
-- update the small token with special action for certain investigators
local SPECIAL_ACTIONS = {
["04002"] = 8, -- Ursula Downs
["01002"] = 9, -- Daisy Walker
["01502"] = 9, -- Daisy Walker
["01002-pb"] = 9, -- Daisy Walker
["06003"] = 10, -- Tony Morgan
["04003"] = 11, -- Finn Edwards
["08016"] = 14 -- Bob Jenkins
2023-01-29 19:31:52 -05:00
}
2024-01-06 21:32:29 -05:00
if smallToken ~= nil then
setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])
2024-01-06 21:32:07 -05:00
end
2024-01-06 21:32:29 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
2024-01-06 21:32:07 -05:00
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
---------------------------------------------------------
-- manipulation of owned objects
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
-- updates the specific owned counter
2024-02-17 19:48:30 -05:00
---@param param table Contains the information to update:
2024-01-06 21:32:29 -05:00
--- type: String Counter to target
--- newValue: Number Value to set the counter to
--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier
function updateCounter(param)
local counter = ownedObjects[param.type]
if counter ~= nil then
counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier))
else
printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- returns the resource counter amount
2024-02-17 19:48:30 -05:00
---@param type string Counter to target
2024-01-06 21:32:29 -05:00
function getCounterValue(type)
return ownedObjects[type].getVar("val")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
-- set investigator skill tracker to "1, 1, 1, 1"
function resetSkillTracker()
local obj = ownedObjects.InvestigatorSkillTracker
if obj ~= nil then
obj.call("updateStats", { 1, 1, 1, 1 })
else
printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow")
end
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
function drawChaosTokenButton(_, _, isRightClick)
2024-02-04 10:51:51 -05:00
chaosBagApi.drawChaosToken(self, isRightClick)
2024-01-06 21:32:29 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
function drawEncounterCard(_, _, isRightClick)
2024-02-04 10:51:51 -05:00
mythosAreaApi.drawEncounterCard(self, isRightClick)
2024-01-06 21:32:29 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
function returnGlobalDiscardPosition()
return self.positionToWorld(DISCARD_PILE_POSITION)
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- Sets this playermat's draw 1 button to visible
2024-02-17 19:48:30 -05:00
---@param visible boolean Whether the draw 1 button should be visible
2024-01-06 21:32:29 -05:00
function showDrawButton(visible)
isDrawButtonVisible = visible
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- create the "Draw 1" button
if isDrawButtonVisible then
self.createButton({
label = "Draw 1",
click_function = "doDrawOne",
function_owner = self,
position = { 1.84, 0.1, -0.36 },
scale = { 0.12, 0.12, 0.12 },
width = 800,
height = 280,
font_size = 180
})
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- remove the "Draw 1" button
else
local buttons = self.getButtons()
for i = 1, #buttons do
if buttons[i].label == "Draw 1" then
self.removeButton(buttons[i].index)
end
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:29 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues
2024-02-17 19:48:30 -05:00
---@param showCounter boolean Whether the clickable clue counter should be visible
2024-01-06 21:32:29 -05:00
function clickableClues(showCounter)
local clickerPos = ownedObjects.ClickableClueCounter.getPosition()
local clueCount = 0
-- move clue counters
local modY = showCounter and 0.525 or -0.525
ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
if showCounter then
-- current clue count
clueCount = ownedObjects.ClueCounter.getVar("exposedValue")
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
-- remove clues
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- set value for clue clickers
ownedObjects.ClickableClueCounter.call("updateVal", clueCount)
else
-- current clue count
clueCount = ownedObjects.ClickableClueCounter.getVar("val")
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- spawn clues
local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
tokenManager.spawnToken(pos, "clue", self.getRotation())
end
2023-01-29 19:31:52 -05:00
end
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
ownedObjects.ClickableClueCounter.call("updateVal", 0)
end
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- reports the clue count
---@param useClickableCounters boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
if useClickableCounters then
return ownedObjects.ClickableClueCounter.getVar("val")
else
return ownedObjects.ClueCounter.getVar("exposedValue")
2023-01-29 19:31:52 -05:00
end
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes
-- is true, the main card slot snap points will only snap assets, while the investigator area point
-- will only snap Investigators. 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.
function setLimitSnapsByType(matchTypes)
local snaps = self.getSnapPoints()
for i, snap in ipairs(snaps) do
local snapPos = snap.position
if inArea(snapPos, MAIN_PLAY_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Asset" }
else
table.insert(snaps[i].tags, "Asset")
end
else
snaps[i].tags = nil
end
end
if inArea(snapPos, INVESTIGATOR_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Investigator" }
else
table.insert(snaps[i].tags, "Investigator")
end
else
snaps[i].tags = nil
end
end
2024-02-04 10:51:51 -05:00
end
2024-02-17 19:48:30 -05:00
self.setSnapPoints(snaps)
end
2023-01-29 19:31:52 -05:00
2024-02-17 19:48:30 -05:00
-- Simple method to check if the given point is in a specified area. Local use only,
---@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. See MAIN_PLAY_AREA for sample
-- bounds definition.
---@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
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
2024-02-04 10:51:51 -05:00
end
end)
2024-03-10 09:56:22 -04:00
__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-04 10:51:51 -05:00
do
2024-03-10 09:56:22 -04:00
local DeckLib = {}
local searchLib = require("util/SearchLib")
2024-02-04 10:51:51 -05:00
2024-03-10 09:56:22 -04:00
-- 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
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- search the new position for existing card/deck
local searchResult = searchLib.atPosition(pos, "isCardOrDeck")
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- 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
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- allow moving the objects smoothly out of the hand
obj.use_hands = false
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
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)
2024-02-04 10:51:51 -05:00
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
return DeckLib
2024-02-04 10:51:51 -05:00
end
end)
2024-03-10 09:56:22 -04:00
__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-04 10:51:51 -05:00
do
2024-03-10 09:56:22 -04:00
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
}
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
-- 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
2024-02-17 19:48:30 -05:00
})
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
-- 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
2023-08-27 21:09:46 -04:00
end
2024-03-10 09:56:22 -04:00
-- searches the specified area
SearchLib.inArea = function(pos, rot, size, filter)
return returnSearchResult(pos, rot, size, filter)
2023-08-27 21:09:46 -04:00
end
2024-03-10 09:56:22 -04:00
-- 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
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
-- searches the specified position (a single point)
SearchLib.atPosition = function(pos, filter)
size = { 0.1, 2, 0.1 }
return returnSearchResult(pos, _, size, filter)
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- 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)
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -04:00
return SearchLib
2022-11-26 20:35:57 -05:00
end
2022-12-13 14:02:30 -05:00
end)
2022-12-13 18:01:00 -05:00
return __bundle_require("__root")