ah_sce_unpacked/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua

2035 lines
67 KiB
Plaintext
Raw Normal View History

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-01-06 21:32:29 -05:00
__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local MythosAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
local function getMythosArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea")
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:29 -05:00
-- returns the chaos token metadata (if provided through scenario reference card)
MythosAreaApi.returnTokenData = function()
return getMythosArea().call("returnTokenData")
end
-- returns an object reference to the encounter deck
MythosAreaApi.getEncounterDeck = function()
return getMythosArea().call("getEncounterDeck")
end
2024-02-04 10:51:51 -05:00
-- draw an encounter card for the requesting mat
MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp)
getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp})
2024-01-06 21:32:29 -05:00
end
return MythosAreaApi
2024-01-06 21:32:07 -05:00
end
2023-08-27 21:09:46 -04:00
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")
2023-01-29 19:31:52 -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)
}
}
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
-- stateIDs for the multi-stated resource tokens
local stateTable = {
["resource"] = 1,
["ammo"] = 2,
["bounty"] = 3,
["charge"] = 4,
["evidence"] = 5,
["secret"] = 6,
["supply"] = 7
}
2022-12-13 14:02:30 -05: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-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
local playerCardData
local locationData
2022-10-19 19:07:47 -04:00
2024-01-06 21:32:07 -05:00
local TokenManager = { }
local internal = { }
2023-08-27 21:09:46 -04: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.
---@param card Object Card to maybe spawn tokens for
---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
--- 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-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Spawns a set of tokens on the given card.
---@param card Object Card to spawn tokens on
---@param tokenType String Type of token to spawn, valid values are "damage", "horror",
-- "resource", "doom", or "clue"
---@param tokenCount Number How many tokens to spawn. For damage or horror this value will be set to the
-- spawned state object rather than spawning multiple tokens
---@param shiftDown Number An offset for the z-value of this group of tokens
2024-02-04 10:51:51 -05:00
---@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()
2020-11-28 13:23:58 -05: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-11-28 13:23:58 -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.
---@param card Object Card to spawn tokens on
---@param tokenType String type of token to spawn, valid values are "damage" and "horror". Other
-- types should use spawnMultipleTokens()
---@param tokenValue Number Value to set the damage/horror to
TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
if tokenValue < 1 or tokenValue > 50 then return end
2023-01-29 19:31:52 -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)
end
2023-04-22 16:56:01 -04:00
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
2022-10-19 19:07:47 -04:00
2024-01-06 21:32:07 -05:00
-- Spawns a number of tokens.
---@param tokenType String type of token to spawn, valid values are resource", "doom", or "clue".
-- Other types should use spawnCounterToken()
---@param tokenCount Number How many tokens to spawn
---@param shiftDown Number An offset for the z-value of this group of tokens
2024-02-04 10:51:51 -05:00
---@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-11-28 13:23:58 -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
2022-12-13 14:02:30 -05: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
offsets = { }
2020-11-28 13:23:58 -05:00
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-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
end
2020-11-28 13:23:58 -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
-- 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
2021-09-03 13:52:12 -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
-- Spawns a single token at the given global position by copying it from the template bag.
---@param position Global position to spawn the token
---@param tokenType String type of token to spawn, valid values are "damage", "horror",
-- "resource", "doom", or "clue"
---@param rotation Vector Rotation to be used for the new token. Only the y-value will be used,
-- x and z will use the default rotation from the source bag
---@param callback function A callback function triggered after the new token is spawned
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]
-- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
local rot = Vector(tokenTemplate.Transform.rotX,
270,
tokenTemplate.Transform.rotZ)
if rotation ~= nil then
rot.y = rotation.y
end
if tokenType == "doom" then
rot.z = 180
end
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
})
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -05:00
-- Checks a card for metadata to maybe replenish it
---@param card Object Card object to be replenished
---@param uses Table The already decoded metadata.uses (to avoid decoding again)
---@param mat Object The playmat the card is placed on (for rotation and casting)
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
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
-- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some
-- callers.
---@param card Object Card object to reset the tokens for
TokenManager.resetTokensSpawned = function(card)
tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())
end
2021-09-03 13:52:12 -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.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addPlayerCardData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
end
end
2022-10-19 19:07:47 -04:00
2024-01-06 21:32:07 -05:00
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addLocationData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- Checks to see if the given card has location data in the DataHelper
---@param card Object Card to check for data
---@return Boolean True if this card has data in the helper, false otherwise
TokenManager.hasLocationData = function(card)
internal.initDataHelperData()
return internal.getLocationData(card) ~= nil
end
2023-08-27 21:09:46 -04: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
2023-08-27 21:09:46 -04:00
end
end
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-11-28 13:23:58 -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.
---@param card Object Card to maybe spawn tokens for
---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
--- 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
2020-11-28 13:23:58 -05: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]
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
-- 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)
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:29 -05:00
2024-01-06 21:32:07 -05:00
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
2022-12-13 14:02:30 -05:00
end
2021-04-10 00:44:08 -04: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.
---@param card Object Card to maybe spawn tokens for
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-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a player card using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
---@param playerData Table Player card data structure retrieved from the DataHelper. Should be
-- 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
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
2024-02-04 10:51:51 -05:00
---@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')
or (not card.is_face_down and locationData.clueSide == 'front')) then
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)
2022-12-13 14:02:30 -05:00
end
2024-01-06 21:32:07 -05:00
return 0
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
-- Gets the right uses structure for this card, based on metadata and face up/down state
---@param card Object Card to pull the uses from
internal.getUses = function(card)
local metadata = JSON.decode(card.getGMNotes()) or { }
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-03-27 10:12:31 -04:00
end
2024-01-06 21:32:07 -05:00
return nil
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
-- Dynamically create positions for clues on a card.
---@param card Object Card the clues will be placed on
---@param count Integer How many clues?
---@return Table Array of global positions to spawn the clues at
internal.buildClueOffsets = function(card, count)
local pos = card.getPosition()
local cluePositions = { }
for i = 1, count do
2024-02-04 11:11:59 -05:00
local column = math.floor((i - 1) / 4)
local row = (i - 1) % 4
table.insert(cluePositions, Vector(pos.x - 1 + 0.55 * row, pos.y, pos.z - 1.4 - 0.55 * column))
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -05:00
return cluePositions
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
---@param card Object Card object to be replenished
---@param uses Table The already decoded metadata.uses (to avoid decoding again)
---@param mat Object The playmat the card is placed on (for rotation and casting)
internal.replenishTokens = function(card, uses, mat)
local cardPos = card.getPosition()
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- don't continue for cards on the deck (Norman) or in the discard pile
if mat.positionToLocal(cardPos).x < -1 then return end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:07 -05:00
-- get current amount of resource tokens on the card
local clickableResourceCounter = nil
local foundTokens = 0
2022-12-13 14:02:30 -05:00
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()
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:07 -05:00
if (stateTable[memo] or 0) > 0 then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
2022-12-13 14:02:30 -05:00
end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:07 -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-01-06 21:32:07 -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
newCount = uses[1].count
end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
-- 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)
end
2022-12-13 14:02:30 -05:00
end
2021-11-16 23:41:43 -05:00
2024-01-06 21:32:07 -05:00
return TokenManager
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -05:00
end)
__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local OptionPanelApi = {}
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
-- loads saved options
---@param options Table New options table
OptionPanelApi.loadSettings = function(options)
return Global.call("loadSettings", options)
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
-- returns option panel table
OptionPanelApi.getOptions = function()
return Global.getTable("optionPanel")
end
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -05:00
return OptionPanelApi
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -05:00
end)
2022-12-13 14:02:30 -05:00
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-01-06 21:32:07 -05:00
local chaosBagApi = require("chaosbag/ChaosBagApi")
2024-02-04 10:51:51 -05:00
local deckLib = require("util/DeckLib")
2024-01-06 21:32:29 -05:00
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-01-06 21:32:07 -05:00
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")
2024-01-06 21:32:07 -05:00
local tokenChecker = require("core/token/TokenChecker")
local tokenManager = require("core/token/TokenManager")
2020-11-28 13:23:58 -05:00
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
-- x-Values for discard buttons
local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91}
2023-08-27 21:09:46 -04:00
local SEARCH_AROUND_SELF_X_BUFFER = 8
2023-04-22 16:56:01 -04:00
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-01-06 21:32:07 -05:00
z = 0.736
2023-01-29 19:31:52 -05:00
},
lowerRight = {
x = -0.79,
2024-01-06 21:32:07 -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-01-06 21:32:07 -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-01-06 21:32:07 -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,
2024-01-06 21:32:29 -05:00
y = 0.5,
2024-01-06 21:32:07 -05:00
z = 0.305
},
size = {
x = 0.4,
2024-01-06 21:32:29 -05:00
y = 3,
2024-01-06 21:32:07 -05:00
z = 1.1
2023-04-22 16:56:01 -04:00
}
2023-01-29 19:31:52 -05:00
}
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- local position of draw and discard pile
2024-01-06 21:32:29 -05:00
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 }
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- global position of encounter discard pile
local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38}
2023-04-22 16:56:01 -04:00
2022-12-13 14:02:30 -05:00
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:29 -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-01-06 21:32:29 -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
})
2023-04-22 16:56:01 -04:00
end
2024-01-06 21:32:07 -05:00
function onLoad(saveState)
2024-02-04 10:51:51 -05:00
self.interactable = false
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:29 -05:00
-- get object references to owned objects
ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)
2020-11-28 13:23:58 -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-01-06 21:32:07 -05:00
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i)
2023-08-27 21:09:46 -04:00
end
2020-12-06 09:42:32 -05:00
self.createButton({
2024-01-06 21:32:07 -05:00
click_function = "drawEncounterCard",
2020-12-06 09:42:32 -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-12-06 09:42:32 -05:00
})
self.createButton({
2023-04-22 16:56:01 -04:00
click_function = "drawChaosTokenButton",
2020-12-06 09:42:32 -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-12-06 09:42:32 -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:07 -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
2020-12-06 09:42:32 -05:00
2024-01-06 21:32:07 -05:00
showDrawButton(isDrawButtonVisible)
collisionEnabled = true
math.randomseed(os.time())
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -05:00
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
2020-11-28 13:23:58 -05:00
2024-01-06 21:32:07 -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)
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:29 -05:00
-- finds all objects on the playmat and associated set aside zone.
2024-01-06 21:32:07 -05:00
function searchAroundSelf(filter)
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
2024-01-06 21:32:07 -05: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)
localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
return searchArea(self.positionToWorld(localCenter), bounds.size, filter)
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -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)
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
function doNotReady(card)
return card.getVar("do_not_ready") or false
2023-04-22 16:56:01 -04:00
end
2024-01-06 21:32:29 -05:00
-- rounds a number to the specified amount of decimal places
---@param num Number Initial value
---@param numDecimalPlaces Number Amount of decimal places
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
2024-01-06 21:32:07 -05:00
-- Discard buttons
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:29 -05:00
-- handles discarding for a list of objects
---@param objList Table List of objects to discard
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})
2023-04-22 16:56:01 -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)
2023-04-22 16:56:01 -04:00
end
2020-11-28 13:23:58 -05:00
end
end
2024-01-06 21:32:07 -05:00
-- build a discard button to discard from searchPosition (number must be unique)
function makeDiscardButton(xValue, number)
local position = { xValue, 0.1, -0.94}
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)
2024-01-06 21:32:07 -05:00
self.createButton({
label = "Discard",
click_function = handlerName,
function_owner = self,
2024-02-04 11:11:59 -05:00
position = {position[1], position[2], position[3] + 0.6},
2024-01-06 21:32:07 -05:00
scale = {0.12, 0.12, 0.12},
width = 900,
height = 350,
font_size = 220
})
2023-04-22 16:56:01 -04:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2024-01-06 21:32:07 -05:00
-- Upkeep button
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- calls the Upkeep function with correct parameter
function doUpkeepFromHotkey(color)
doUpkeep(_, color)
2020-12-06 09:42:32 -05:00
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
function doUpkeep(_, clickedByColor, isRightClick)
if isRightClick then
changeColor(clickedByColor)
2023-08-27 21:09:46 -04:00
return
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or clickedByColor
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -05:00
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
local forcedLearning = false
local rot = self.getRotation()
for _, obj in ipairs(searchAroundSelf()) do
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
if not doNotReady(obj) then
local cardRotation = round(obj.getRotation().y, 0) - rot.y
local yRotDiff = 0
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
if cardRotation < 0 then
cardRotation = cardRotation + 360
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- 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
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- 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})
end
if cardMetadata.id == "08031" then
forcedLearning = true
end
if cardMetadata.uses ~= nil then
tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)
end
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -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)
if activeInvestigatorId ~= nil then
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()
end
end
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
-- gain a resource (or two if playing Jenny Barnes)
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
2024-01-06 21:32:29 -05:00
updateCounter({type = "ResourceCounter", modifier = 2})
2024-01-06 21:32:07 -05:00
printToColor("Gaining 2 resources (Jenny)", messageColor)
else
2024-01-06 21:32:29 -05:00
updateCounter({type = "ResourceCounter", modifier = 1})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
2024-01-06 21:32:07 -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
local handSize = #Player[playerColor].getHandObjects()
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
2023-04-22 16:56:01 -04:00
end
2024-01-06 21:32:07 -05:00
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
elseif activeInvestigatorId == "89001" then
printToColor("Drawing 2 cards (Subject 5U-21)", messageColor)
drawCardsWithReshuffle(2)
else
drawCardsWithReshuffle(1)
2023-04-22 16:56:01 -04:00
end
end
2024-01-06 21:32:07 -05:00
-- function for "draw 1 button" (that can be added via option panel)
function doDrawOne(_, color)
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or color
drawCardsWithReshuffle(1)
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
2024-01-06 21:32:29 -05:00
local deckAreaObjects = getDeckAreaObjects()
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -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
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
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
2023-01-29 19:31:52 -05:00
return
end
2022-12-13 14:02:30 -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
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2024-01-06 21:32:07 -05:00
local deckSize = 1
2024-01-06 21:32:29 -05:00
if deckAreaObjects.draw == nil then
2024-01-06 21:32:07 -05:00
deckSize = 0
2024-01-06 21:32:29 -05:00
elseif deckAreaObjects.draw.type == "Deck" then
deckSize = #deckAreaObjects.draw.getObjects()
2022-10-19 19:07:47 -04:00
end
2024-01-06 21:32:07 -05:00
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-10-19 19:07:47 -04:00
end
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:07 -05:00
if self.positionToLocal(object.getPosition()).z > 0.5 then
2024-01-06 21:32:29 -05:00
deckAreaObjects.discard = object
2024-01-06 21:32:07 -05:00
-- Norman Withers handling
2024-01-06 21:32:29 -05:00
elseif object.type == "Card" and not object.is_face_down then
deckAreaObjects.topCard = object
2024-01-06 21:32:07 -05:00
else
2024-01-06 21:32:29 -05:00
deckAreaObjects.draw = object
2024-01-06 21:32:07 -05:00
end
end
2024-01-06 21:32:29 -05:00
return deckAreaObjects
2020-11-28 13:23:58 -05:00
end
2024-01-06 21:32:07 -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
2024-01-06 21:32:07 -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)
2023-01-29 19:31:52 -05:00
end
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
return
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 })
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:29 -05:00
end, 0.1)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05: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")
2023-01-29 19:31:52 -05:00
else
2024-01-06 21:32:07 -05:00
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)
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
if #choices == 0 then
broadcastToAll("Hidden cards can't be randomly discarded.", "Orange")
return
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- 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-01-06 21:32:07 -05:00
broadcastToAll(playerColor .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White")
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
---------------------------------------------------------
-- color related functions
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- changes the player color
function changeColor(clickedByColor)
local colorList = {
"White",
"Brown",
"Red",
"Orange",
"Yellow",
"Green",
"Teal",
"Blue",
"Purple",
"Pink"
}
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- 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
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
-- 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
2024-01-06 21:32:07 -05:00
handZone.setValue(color)
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- if the seated player clicked this, reseat him to the new color
if clickedByColor == playerColor then
navigationOverlayApi.copyVisibility(playerColor, color)
Player[playerColor].changeColor(color)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- update the internal variable
playerColor = color
end)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05: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
2024-01-06 21:32:07 -05:00
syncCustomizableMetadata(card)
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
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
2024-01-06 21:32:07 -05: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
end
2023-01-29 19:31:52 -05:00
end
end
end
end
end
2024-01-06 21:32:07 -05:00
function spawnTokensFor(object)
local extraUses = { }
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
end
2021-01-02 22:49:38 -05:00
2024-01-06 21:32:07 -05:00
tokenManager.spawnForCard(object, extraUses)
2021-01-02 22:49:38 -05:00
end
2024-01-06 21:32:29 -05:00
function onCollisionEnter(collisionInfo)
local object = collisionInfo.collision_object
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- only continue if loading is completed
if not collisionEnabled then return end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- only continue for cards
2024-02-04 10:51:51 -05:00
if object.type ~= "Card" then return end
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
2024-01-06 21:32:07 -05:00
maybeUpdateActiveInvestigator(object)
syncCustomizableMetadata(object)
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
local localCardPos = self.positionToLocal(object.getPosition())
if inArea(localCardPos, DECK_DISCARD_AREA) then
tokenManager.resetTokensSpawned(object)
removeTokensFromObject(object)
elseif shouldSpawnTokens(object) then
spawnTokensFor(object)
2023-01-29 19:31:52 -05:00
end
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05: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-08-27 21:09:46 -04:00
end
2024-01-06 21:32:07 -05:00
-- checks if tokens should be spawned for the provided card
function shouldSpawnTokens(card)
if card.is_face_down then
return false
2023-08-27 21:09:46 -04:00
end
2024-01-06 21:32:07 -05:00
local localCardPos = self.positionToLocal(card.getPosition())
local metadata = JSON.decode(card.getGMNotes())
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -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-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for assets and events on the main area
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Spawn tokens for all encounter types in the threat area
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
or metadata.type == "Enemy"
or metadata.weakness) then
return true
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
return false
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -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
2024-01-06 21:32:07 -05:00
tokenManager.resetTokensSpawned(object)
2024-01-06 21:32:29 -05:00
removeTokensFromObject(object)
2024-01-06 21:32:07 -05:00
end
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -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
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
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:07 -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)
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
---------------------------------------------------------
-- investigator ID grabbing and skill tracker
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
function maybeUpdateActiveInvestigator(card)
if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
local notes = JSON.decode(card.getGMNotes())
local class
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
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
2024-01-06 21:32:29 -05:00
ownedObjects.InvestigatorSkillTracker.call("updateStats", {
notes.willpowerIcons,
notes.intellectIcons,
notes.combatIcons,
notes.agilityIcons
})
2024-01-06 21:32:07 -05:00
elseif activeInvestigatorId ~= "00000" then
class = "Neutral"
activeInvestigatorId = "00000"
2024-01-06 21:32:29 -05:00
ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1})
2024-01-06 21:32:07 -05:00
else
return
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -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
}
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
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
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -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:07 -05:00
if smallToken ~= nil then
setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
---------------------------------------------------------
-- manipulation of owned objects
---------------------------------------------------------
-- updates the specific owned counter
---@param param Table Contains the information to update:
--- 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
---@param type String Counter to target
function getCounterValue(type)
return ownedObjects[type].getVar("val")
end
2023-01-29 19:31:52 -05:00
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 })
2023-01-29 19:31:52 -05:00
else
2024-01-06 21:32:29 -05:00
printToAll("Skill tracker for " .. matColor .. " playmat 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:07 -05:00
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
function drawChaosTokenButton(_, _, isRightClick)
2024-02-04 10:51:51 -05:00
chaosBagApi.drawChaosToken(self, isRightClick)
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
function drawEncounterCard(_, _, isRightClick)
2024-02-04 10:51:51 -05:00
mythosAreaApi.drawEncounterCard(self, isRightClick)
2024-01-06 21:32:07 -05:00
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
function returnGlobalDiscardPosition()
return self.positionToWorld(DISCARD_PILE_POSITION)
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Sets this playermat's draw 1 button to visible
---@param visible Boolean. Whether the draw 1 button should be visible
function showDrawButton(visible)
isDrawButtonVisible = visible
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -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
})
-- 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
2022-12-13 14:02:30 -05:00
end
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
-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues
---@param showCounter Boolean Whether the clickable clue counter should be visible
2024-01-06 21:32:07 -05:00
function clickableClues(showCounter)
2024-01-06 21:32:29 -05:00
local clickerPos = ownedObjects.ClickableClueCounter.getPosition()
2024-01-06 21:32:07 -05:00
local clueCount = 0
2024-01-06 21:32:29 -05:00
-- 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:07 -05:00
if showCounter then
-- current clue count
2024-01-06 21:32:29 -05:00
clueCount = ownedObjects.ClueCounter.getVar("exposedValue")
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- remove clues
2024-01-06 21:32:29 -05:00
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- set value for clue clickers
2024-01-06 21:32:29 -05:00
ownedObjects.ClickableClueCounter.call("updateVal", clueCount)
2024-01-06 21:32:07 -05:00
else
-- current clue count
2024-01-06 21:32:29 -05:00
clueCount = ownedObjects.ClickableClueCounter.getVar("val")
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -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())
2023-08-27 21:09:46 -04:00
end
end
2024-01-06 21:32:07 -05:00
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
2024-01-06 21:32:29 -05:00
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
ownedObjects.ClickableClueCounter.call("updateVal", 0)
2024-01-06 21:32:07 -05:00
end
-- reports the clue count
---@param useClickableCounters Boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
if useClickableCounters then
2024-01-06 21:32:29 -05:00
return ownedObjects.ClickableClueCounter.getVar("val")
2024-01-06 21:32:07 -05:00
else
2024-01-06 21:32:29 -05:00
return ownedObjects.ClueCounter.getVar("exposedValue")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -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
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
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
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
self.setSnapPoints(snaps)
end
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- Simple method to check if the given point is in a specified area. Local use only,
---@param point 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
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:07 -05:00
-- 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)
end
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)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- 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)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
return chaosBagContents
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- checks scripting zone for chaos bag (also called by a lot of objects!)
ChaosBagApi.findChaosBag = function()
return Global.call("findChaosBag")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- returns a table of object references to the tokens in play (does not include sealed tokens!)
ChaosBagApi.getTokensInPlay = function()
2024-02-04 10:51:51 -05:00
return Global.call("getChaosTokensinPlay")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- returns all sealed tokens on cards to the chaos bag
ChaosBagApi.releaseAllSealedTokens = function(playerColor)
return Global.call("releaseAllSealedTokens", playerColor)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- returns all drawn tokens to the chaos bag
ChaosBagApi.returnChaosTokens = function(playerColor)
return Global.call("returnChaosTokens", playerColor)
2023-01-29 19:31:52 -05:00
end
2023-08-27 21:09:46 -04:00
2024-01-06 21:32:07 -05:00
-- 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)
2023-01-29 19:31:52 -05:00
end
2024-02-04 10:51:51 -05:00
-- returns a chaos token to the bag and calls all relevant functions
---@param token TTSObject Chaos Token to return
ChaosBagApi.returnChaosTokenToBag = function(token)
return Global.call("returnChaosTokenToBag", token)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- 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)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- 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 Boolean. True if the bag is manipulated, false if it should be blocked.
ChaosBagApi.canTouchChaosTokens = function()
return Global.call("canTouchChaosTokens")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- called by playermats (by the "Draw chaos token" button)
2024-02-04 10:51:51 -05:00
ChaosBagApi.drawChaosToken = function(mat, drawAdditional)
return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional})
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
-- 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")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:07 -05:00
return ChaosBagApi
end
end)
2024-02-04 10:51:51 -05:00
__bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local NavigationOverlayApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
local function getNOHandler()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "NavigationOverlayHandler")
2023-01-29 19:31:52 -05:00
end
2024-02-04 10:51:51 -05: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
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05: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)
2023-01-29 19:31:52 -05:00
end
2024-02-04 10:51:51 -05:00
-- loads the specified camera for a player
---@param player TTSPlayerInstance Player whose camera should be moved
---@param camera Variant 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
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
return NavigationOverlayApi
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
2023-01-29 19:31:52 -05:00
end
2024-02-04 10:51:51 -05:00
return TokenChecker
end
end)
__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local DeckLib = {}
local searchLib = require("util/SearchLib")
-- places a card/deck at a position or merges into an existing deck
---@param obj TTSObject Object to move
---@param pos Table New position for the object
---@param rot Table New rotation for the object (optional)
DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot)
if obj == nil or pos == nil then return end
-- search the new position for existing card/deck
local searchResult = searchLib.atPosition(pos, "isCardOrDeck")
-- get new position
local newPos
local offset = 0.5
if #searchResult == 1 then
local bounds = searchResult[1].getBounds()
newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset)
else
newPos = Vector(pos) + Vector(0, offset, 0)
end
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05:00
-- allow moving the objects smoothly out of the hand
obj.use_hands = false
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05: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)
end
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05:00
return DeckLib
end
end)
__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local SearchLib = {}
local filterFunctions = {
isActionToken = function(x) return x.getDescription() == "Action Token" end,
isCard = function(x) return x.type == "Card" end,
isDeck = function(x) return x.type == "Deck" end,
isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end,
isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end,
isTileOrToken = function(x) return x.type == "Tile" end
}
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05:00
-- performs the actual search and returns a filtered list of object references
---@param pos Table Global position
---@param rot Table 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)
if filter then filter = filterFunctions[filter] end
local searchResult = Physics.cast({
origin = pos,
direction = direction or { 0, 1, 0 },
orientation = rot or { 0, 0, 0 },
type = 3,
size = size,
max_distance = maxDistance or 0
})
-- filtering the result
local objList = {}
for _, v in ipairs(searchResult) do
if not filter or filter(v.hit_object) then
table.insert(objList, v.hit_object)
2023-08-27 21:09:46 -04:00
end
end
2024-02-04 10:51:51 -05:00
return objList
end
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05:00
-- searches the specified area
SearchLib.inArea = function(pos, rot, size, filter)
return returnSearchResult(pos, rot, size, filter)
end
2023-08-27 21:09:46 -04:00
2024-02-04 10:51:51 -05: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-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
-- searches the specified position (a single point)
SearchLib.atPosition = function(pos, filter)
size = { 0.1, 2, 0.1 }
return returnSearchResult(pos, _, size, filter)
2023-08-27 21:09:46 -04:00
end
2024-02-04 10:51:51 -05: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-08-27 21:09:46 -04:00
end
2024-02-04 10:51:51 -05:00
return SearchLib
2023-01-29 19:31:52 -05:00
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
2024-02-04 10:51:51 -05:00
local PlayAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
local function getPlayArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea")
end
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
local function getInvestigatorCounter()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter")
end
2023-08-27 21:09:46 -04:00
2023-01-29 19:31:52 -05:00
-- Returns the current value of the investigator counter from the playmat
---@return Integer. Number of investigators currently set on the counter
PlayAreaApi.getInvestigatorCount = function()
2024-02-04 10:51:51 -05:00
return getInvestigatorCounter().getVar("val")
2023-01-29 19:31:52 -05:00
end
2023-08-27 21:09:46 -04:00
-- 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)
2024-02-04 10:51:51 -05:00
getInvestigatorCounter().call("updateVal", count)
2023-08-27 21:09:46 -04:00
end
2023-01-29 19:31:52 -05:00
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
2024-02-04 10:51:51 -05:00
-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'
---@param playerColor Color Color of the player requesting the shift for messages
2023-01-29 19:31:52 -05:00
PlayAreaApi.shiftContentsUp = function(playerColor)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("shiftContentsUp", playerColor)
2023-01-29 19:31:52 -05:00
end
PlayAreaApi.shiftContentsDown = function(playerColor)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("shiftContentsDown", playerColor)
2023-01-29 19:31:52 -05:00
end
PlayAreaApi.shiftContentsLeft = function(playerColor)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("shiftContentsLeft", playerColor)
2023-01-29 19:31:52 -05:00
end
PlayAreaApi.shiftContentsRight = function(playerColor)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("shiftContentsRight", playerColor)
2023-01-29 19:31:52 -05:00
end
-- Reset the play area's tracking of which cards have had tokens spawned.
PlayAreaApi.resetSpawnedCards = function()
2024-02-04 10:51:51 -05:00
return getPlayArea().call("resetSpawnedCards")
end
-- Sets whether location connections should be drawn
PlayAreaApi.setConnectionDrawState = function(state)
getPlayArea().call("setConnectionDrawState", state)
end
-- Sets the connection color
PlayAreaApi.setConnectionColor = function(color)
getPlayArea().call("setConnectionColor", color)
2023-01-29 19:31:52 -05:00
end
-- Event to be called when the current scenario has changed.
---@param scenarioName Name of the new scenario
PlayAreaApi.onScenarioChanged = function(scenarioName)
2024-02-04 10:51:51 -05:00
getPlayArea().call("onScenarioChanged", scenarioName)
2023-01-29 19:31:52 -05:00
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.
2024-02-04 10:51:51 -05:00
---@param matchCardTypes Boolean Whether snap points should only snap for the matching card types.
2023-01-29 19:31:52 -05:00
PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
2024-02-04 10:51:51 -05:00
getPlayArea().call("setLimitSnapsByType", matchCardTypes)
2023-01-29 19:31:52 -05:00
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)
2024-02-04 10:51:51 -05:00
getPlayArea().call("tryObjectEnterContainer", { container = container, object = object })
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- counts the VP on locations in the play area
PlayAreaApi.countVP = function()
2024-02-04 10:51:51 -05:00
return getPlayArea().call("countVP")
2023-04-22 16:56:01 -04:00
end
-- highlights all locations in the play area without metadata
---@param state Boolean True if highlighting should be enabled
PlayAreaApi.highlightMissingData = function(state)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("highlightMissingData", state)
2023-04-22 16:56:01 -04:00
end
-- highlights all locations in the play area with VP
---@param state Boolean True if highlighting should be enabled
PlayAreaApi.highlightCountedVP = function(state)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("countVP", state)
2023-04-22 16:56:01 -04:00
end
-- Checks if an object is in the play area (returns true or false)
PlayAreaApi.isInPlayArea = function(object)
2024-02-04 10:51:51 -05:00
return getPlayArea().call("isInPlayArea", object)
2023-04-22 16:56:01 -04:00
end
2023-08-27 21:09:46 -04:00
PlayAreaApi.getSurface = function()
2024-02-04 10:51:51 -05:00
return getPlayArea().getCustomObject().image
2023-08-27 21:09:46 -04:00
end
PlayAreaApi.updateSurface = function(url)
2024-02-04 10:51:51 -05:00
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")
2023-08-27 21:09:46 -04:00
end
2023-01-29 19:31:52 -05:00
return PlayAreaApi
end
end)
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
2024-02-04 10:51:51 -05:00
local TokenSpawnTracker = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2023-01-29 19:31:52 -05:00
2024-02-04 10:51:51 -05:00
local function getSpawnTracker()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker")
end
2023-01-29 19:31:52 -05:00
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("hasSpawnedTokens", cardGuid)
2023-01-29 19:31:52 -05:00
end
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("markTokensSpawned", cardGuid)
2023-01-29 19:31:52 -05:00
end
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("resetTokensSpawned", cardGuid)
2023-01-29 19:31:52 -05:00
end
TokenSpawnTracker.resetAllAssetAndEvents = function()
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("resetAllAssetAndEvents")
2023-01-29 19:31:52 -05:00
end
TokenSpawnTracker.resetAllLocations = function()
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("resetAllLocations")
2023-01-29 19:31:52 -05:00
end
TokenSpawnTracker.resetAll = function()
2024-02-04 10:51:51 -05:00
return getSpawnTracker().call("resetAll")
2023-01-29 19:31:52 -05:00
end
return TokenSpawnTracker
2022-12-13 14:02:30 -05:00
end
end)
2024-02-04 10:51:51 -05:00
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("playermat/Playmat")
end)
2024-01-06 21:32:29 -05:00
__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-01-06 21:32:07 -05:00
do
2024-01-06 21:32:29 -05:00
local GUIDReferenceApi = {}
2023-01-29 19:31:52 -05:00
2024-01-06 21:32:29 -05:00
local function getGuidHandler()
return getObjectFromGUID("123456")
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
-- returns all matching objects as a table with references
---@param owner String Parent object for this search
---@param type String Type of object to search for
GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)
return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type })
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
-- returns all matching objects as a table with references
---@param type String Type of object to search for
GUIDReferenceApi.getObjectsByType = function(type)
return getGuidHandler().call("getObjectsByType", type)
2023-01-29 19:31:52 -05:00
end
2024-01-06 21:32:29 -05:00
-- returns all matching objects as a table with references
---@param owner String Parent object for this search
GUIDReferenceApi.getObjectsByOwner = function(owner)
return getGuidHandler().call("getObjectsByOwner", owner)
2023-01-29 19:31:52 -05:00
end
2024-02-04 10:51:51 -05:00
-- 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
2024-01-06 21:32:29 -05:00
return GUIDReferenceApi
2022-12-13 14:02:30 -05:00
end
end)
2022-12-13 18:01:00 -05:00
return __bundle_require("__root")