token manager including

This commit is contained in:
Chr1Z93 2024-08-03 23:19:52 +02:00
parent 5eb6873c45
commit 96f251183f
8 changed files with 636 additions and 557 deletions

View File

@ -8,7 +8,7 @@ local searchLib = require("util/SearchLib")
local soundCubeApi = require("core/SoundCubeApi")
local tokenArrangerApi = require("accessories/TokenArrangerApi")
local tokenChecker = require("core/token/TokenChecker")
local tokenManager = require("core/token/TokenManager")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
---------------------------------------------------------
-- general setup
@ -105,6 +105,26 @@ ID_URL_MAP = {
['frost'] = { name = "Frost", url = 'https://steamusercontent-a.akamaihd.net/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/' }
}
TokenManager = {}
local tokenOffsets = {}
-- 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
local playerCardData, locationData
-- stateIDs for the multi-stated resource tokens
local stateTable = {
["resource"] = 1,
["ammo"] = 2,
["bounty"] = 3,
["charge"] = 4,
["evidence"] = 5,
["secret"] = 6,
["supply"] = 7,
["offering"] = 8
}
---------------------------------------------------------
-- general code
---------------------------------------------------------
@ -148,6 +168,7 @@ function onLoad(savedData)
getModVersion()
math.randomseed(os.time())
TokenManager.initialiize()
-- initialization of loadable objects library (delay to let Navigation Overlay build)
Wait.time(function()
@ -566,11 +587,15 @@ end
-- token spawning
---------------------------------------------------------
-- DEPRECATED. Use TokenManager instead.
-- DEPRECATED. Use TokenManager instead --> TODO: Remove this with the new downloads repo (v.4.0.0)
-- Spawns a single token.
---@param params table Array with arguments to the method. 1 = position, 2 = type, 3 = rotation
function spawnToken(params)
return tokenManager.spawnToken(params[1], params[2], params[3])
return TokenManager.spawnToken({
position = params[1],
tokenType = params[2],
rotation = params[3]
})
end
---------------------------------------------------------
@ -1153,12 +1178,9 @@ function onClick_toggleUi(player, windowId)
return
end
-- hide the playAreaGallery if visible
if windowId == "downloadWindow" then
changeWindowVisibilityForColor(player.color, "playAreaGallery", false)
-- hide the downloadWindow if visible
elseif windowId == "playAreaGallery" then
changeWindowVisibilityForColor(player.color, "downloadWindow", false)
-- hide the playAreaGallery / downloadWindow if visible
if windowId == "downloadWindow" or windowId == "playAreaGallery" then
changeWindowVisibilityForColor(player.color, windowId, false)
end
changeWindowVisibilityForColor(player.color, windowId)
@ -1236,7 +1258,8 @@ function updatePreviewWindow()
-- set default image if not defined
if item.boxsize == nil or item.boxsize == "" or item.boxart == nil or item.boxart == "" then
item.boxsize = "big"
item.boxart = "https://steamusercontent-a.akamaihd.net/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/"
item.boxart =
"https://steamusercontent-a.akamaihd.net/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/"
end
UI.setValue("previewTitle", item.name)
@ -1883,6 +1906,479 @@ function onClick_notification(_, parameter)
UI.hide("updateNotification")
end
---------------------------------------------------------
-- Token Manager
---------------------------------------------------------
function TokenManager.initialiize()
TokenManager.generateOffsets(12)
end
-- Generates the offsets for tokens on a card (clues on locations are different and have their own function)
---@param maxTokens number Maximum amount of tokens on a card
function TokenManager.generateOffsets(maxTokens)
tokenOffsets = {}
for numTokens = 1, maxTokens do
if numTokens == 1 then
tokenOffsets[1] = Vector(0, 3, -0.2)
else
local offsets = {}
local rows = math.min(4, math.ceil(numTokens / 3))
local tokensPlaced = 0
for row = 1, rows do
local y = 3
local z = -0.9 + (row - 1) * 0.7
local tokensInRow = math.min(3, numTokens - tokensPlaced)
for col = 1, tokensInRow do
local x = 0
if tokensInRow == 2 then
x = col == 1 and -0.4 or 0.4
elseif tokensInRow == 3 then
x = (col - 2) * 0.7
end
table.insert(offsets, Vector(x, y, z))
tokensPlaced = tokensPlaced + 1
end
end
tokenOffsets[numTokens] = offsets
end
end
end
-- 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.
function TokenManager.spawnForCard(params)
if tokenSpawnTrackerApi.hasSpawnedTokens(params.card.getGUID()) then return end
local metadata = JSON.decode(params.card.getGMNotes())
if metadata ~= nil then
TokenManager.spawnTokensFromUses(params.card, params.extraUses)
else
TokenManager.spawnTokensFromDataHelper(params.card)
end
end
-- Spawns a set of tokens on the given card.
function TokenManager.spawnTokenGroup(param)
local card = param.card
local tokenType = param.tokenType
local tokenCount = param.tokenCount
local shiftDown = param.shiftDown
local subType = param.subType
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
-- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.
---@param card tts__Object Card to spawn tokens on
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@param tokenValue number Value to set the damage/horror to
function TokenManager.spawnCounterToken(card, tokenType, tokenValue, shiftDown)
if tokenValue < 1 or tokenValue > 50 then return end
local pos = card.positionToWorld(tokenOffsets[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
TokenManager.spawnToken({
position = pos,
tokenType = tokenType,
rotation = rot,
callback = function(spawned)
-- token starts in state 1, so don't attempt to change it to avoid error
if tokenValue ~= 1 then
spawned.setState(tokenValue)
end
end
})
end
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({
position = pos,
tokenType = "resourceCounter",
rotation = rot,
callback = function(spawned)
spawned.call("updateVal", tokenCount)
end
})
end
-- Spawns a number of tokens.
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@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 or action tokens
function TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)
if tokenCount < 1 then return end
local offsets = {}
if tokenType == "clue" then
offsets = TokenManager.buildClueOffsets(card, tokenCount)
else
if tokenCount > 12 then
printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.")
TokenManager.spawnResourceCounterToken(card, tokenCount)
return
end
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(tokenOffsets[tokenCount][i])
end
end
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = {}
-- get a vector for the shifting (downwards local to the card)
local shiftDownVector = Vector(0, 0, shiftDown):rotateOver("y", card.getRotation().y)
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset + shiftDownVector
end
end
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
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 or "")]
if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then
callback = function(spawned) spawned.setState(stateID) end
elseif tokenType == "universalActionAbility" then
local matColor = playermatApi.getMatColorByPosition(card.getPosition())
local class = playermatApi.returnInvestigatorClass(matColor)
callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end
end
for i = 1, tokenCount do
TokenManager.spawnToken({
position = offsets[i],
tokenType = tokenType,
rotation = card.getRotation(),
callback = callback
})
end
end
-- Spawns a single token at the given global position by copying it from the template bag.
function TokenManager.spawnToken(params)
local position = params.position
local rotation = params.rotation
local tokenType = params.tokenType
local callback = params.callback
TokenManager.initTokenTemplates()
local loadTokenType = tokenType
if tokenType == "clue" or tokenType == "doom" then
loadTokenType = "clueDoom"
end
local tokenTemplate = tokenTemplates[loadTokenType]
if tokenTemplate == nil then
error("Unknown token type '" .. loadTokenType .. "'")
return
end
-- 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
})
end
-- Checks a card for metadata to maybe replenish it
function TokenManager.maybeReplenishCard(params)
for _, useInfo in ipairs(params.uses) do
if useInfo.count and useInfo.replenish then
TokenManager.replenishTokens(params.card, useInfo)
end
end
end
-- 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
function TokenManager.addPlayerCardData(dataTable)
TokenManager.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
end
end
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable table Key/Value pairs following the DataHelper style
function TokenManager.addLocationData(dataTable)
TokenManager.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
end
-- Checks to see if the given card has location data in the DataHelper
---@param card tts__Object Card to check for data
---@return boolean: True if this card has data in the helper, false otherwise
function TokenManager.hasLocationData(card)
TokenManager.initDataHelperData()
return TokenManager.getLocationData(card) ~= nil
end
function TokenManager.initTokenTemplates()
if tokenTemplates ~= nil then return end
tokenTemplates = {}
local tokenSource = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSource")
for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do
local tokenName = tokenTemplate.Memo
tokenTemplates[tokenName] = tokenTemplate
end
end
-- Copies the data from the DataHelper. Will only happen once.
function TokenManager.initDataHelperData()
if playerCardData ~= nil then return end
local dataHelper = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper")
playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')
locationData = dataHelper.getTable('LOCATIONS_DATA')
end
-- 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 tts__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 playermat should pass "Charge"=1
function TokenManager.spawnTokensFromUses(card, extraUses)
local uses = TokenManager.getUses(card)
if uses == nil then return end
-- go through tokens to spawn
local tokenCount
for i, useInfo in ipairs(uses) do
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]
end
-- Shift each spawned group after the first down so they don't pile on each other
TokenManager.spawnTokenGroup({
card = card,
tokenType = useInfo.token,
tokenCount = tokenCount,
shiftDown = (i - 1) * 0.8,
subType = useInfo.type
})
end
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
-- 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 tts__Object Card to maybe spawn tokens for
function TokenManager.spawnTokensFromDataHelper(card)
TokenManager.initDataHelperData()
local playerData = TokenManager.getPlayerCardData(card)
if playerData ~= nil then
TokenManager.spawnPlayerCardTokensFromDataHelper(card, playerData)
end
local specificLocationData = TokenManager.getLocationData(card)
if specificLocationData ~= nil then
TokenManager.spawnLocationTokensFromDataHelper(card, specificLocationData)
end
end
-- Spawn tokens for a player card using data retrieved from the Data Helper.
---@param card tts__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.
function TokenManager.spawnPlayerCardTokensFromDataHelper(card, playerData)
TokenManager.spawnTokenGroup({
card = card,
tokenType = playerData.tokenType,
tokenCount = playerData.tokenCount
})
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card tts__Object Card to maybe spawn tokens for
---@param locationData table Location data structure retrieved from the DataHelper. Should be
-- the right data for this card.
function TokenManager.spawnLocationTokensFromDataHelper(card, locationData)
local clueCount = TokenManager.getClueCountFromData(card, locationData)
if clueCount > 0 then
TokenManager.spawnTokenGroup({ card = card, tokenType = "clue", tokenCount = clueCount })
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
end
function TokenManager.getPlayerCardData(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
function TokenManager.getLocationData(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
end
function TokenManager.getClueCountFromData(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
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()
end
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
---@param card tts__Object Card to pull the uses from
TokenManager.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
end
elseif not card.is_face_down then
return metadata.uses
end
return nil
end
-- Dynamically create positions for clues on a card
---@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
TokenManager.buildClueOffsets = function(card, count)
-- make sure clues always spawn from left to right
local modifier = card.is_face_down and 1 or -1
local cluePositions = {}
for i = 1, count do
-- get the set number (1 for clue 1-16, 2 for 17-32 etc.)
local set = math.floor((i - 1) / 16) + 1
-- get the local index (always number from 1-16)
local localIndex = (i - 1) % 16
-- get row and column for this clue
local row = math.floor(localIndex / 4) + 1
local column = localIndex % 4
-- calculate local position
local localPos = Vector((-0.825 + 0.55 * column) * modifier, 0, -1.5 + 0.55 * row)
-- get the global clue position (higher y-position for each set)
local cluePos = card.positionToWorld(localPos) + Vector(0, 0.03 + 0.103 * (set - 1), 0)
-- add position to table
table.insert(cluePositions, cluePos)
end
return cluePositions
end
---@param card tts__Object Card object to be replenished
---@param useInfo table The already decoded subtable of metadata.uses (to avoid decoding again)
TokenManager.replenishTokens = function(card, useInfo)
-- get current amount of matching resource tokens on the card
local clickableResourceCounter = nil
local foundTokens = 0
local maybeDeleteThese = {}
if useInfo.token == "clue" then
for _, obj in ipairs(searchLib.onObject(card, "isClue")) do
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
end
elseif useInfo.token == "doom" then
for _, obj in ipairs(searchLib.onObject(card, "isDoom")) do
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
end
else
-- search for the token instead if there's no special resource state for it
local searchType = string.lower(useInfo.type)
if stateTable[searchType] == nil then
searchType = useInfo.token
end
for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
local memo = obj.getMemo()
if searchType == memo then
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
end
end
-- this is the theoretical new amount of uses (to be checked below)
local newCount = foundTokens + useInfo.replenish
-- if there are already more uses than the replenish amount, keep them
if foundTokens > useInfo.count then
newCount = foundTokens
-- only replenish up until the replenish amount
elseif newCount > useInfo.count then
newCount = useInfo.count
end
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
-- delete existing tokens
for _, obj in ipairs(maybeDeleteThese) do
obj.destruct()
end
-- spawn new token group
TokenManager.spawnTokenGroup({
card = card,
tokenType = useInfo.token,
tokenCount = newCount,
subType = useInfo.type
})
end
end
---------------------------------------------------------
-- Utility functions
---------------------------------------------------------

View File

@ -1,6 +1,6 @@
local guidReferenceApi = require("core/GUIDReferenceApi")
local searchLib = require("util/SearchLib")
local tokenManager = require("core/token/TokenManager")
local tokenManagerApi = require("core/token/TokenManagerApi")
-- Location connection directional options
local BIDIRECTIONAL = 0
@ -80,7 +80,7 @@ end
function updateLocations(args)
customDataHelper = getObjectFromGUID(args[1])
if customDataHelper ~= nil then
tokenManager.addLocationData(customDataHelper.getTable("LOCATIONS_DATA"))
tokenManagerApi.addLocationData(customDataHelper.getTable("LOCATIONS_DATA"))
end
end
@ -102,7 +102,7 @@ function onCollisionEnter(collisionInfo)
-- check if we should spawn clues here and do so according to playercount
if shouldSpawnTokens(object) then
tokenManager.spawnForCard(object)
tokenManagerApi.spawnForCard(object)
end
-- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send
@ -192,7 +192,7 @@ end
function shouldSpawnTokens(card)
local metadata = JSON.decode(card.getGMNotes())
if metadata == nil then
return tokenManager.hasLocationData(card)
return tokenManagerApi.hasLocationData(card)
end
return metadata.type == "Location"
or metadata.type == "Enemy"

View File

@ -1,486 +0,0 @@
do
local GlobalApi = require("core/GlobalApi")
local guidReferenceApi = require("core/GUIDReferenceApi")
local playAreaApi = require("core/PlayAreaApi")
local playermatApi = require("playermat/PlayermatApi")
local searchLib = require("util/SearchLib")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
local TokenManager = {}
local internal = {}
-- stateIDs for the multi-stated resource tokens
local stateTable = {
["resource"] = 1,
["ammo"] = 2,
["bounty"] = 3,
["charge"] = 4,
["evidence"] = 5,
["secret"] = 6,
["supply"] = 7,
["offering"] = 8
}
-- 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
local playerCardData
local locationData
function internal.generateOffsets(maxTokens)
local totalOffsets = {}
for numTokens = 1, maxTokens do
local offsets = {}
local rows = math.min(4, math.ceil(numTokens / 3))
local tokensPlaced = 0
for row = 1, rows do
local y = 3
local z = -0.9 + (row - 1) * 0.7
local tokensInRow = math.min(3, numTokens - tokensPlaced)
for col = 1, tokensInRow do
local x = 0
if tokensInRow == 2 then
x = col == 1 and -0.4 or 0.4
elseif tokensInRow == 3 then
x = (col - 2) * 0.7
end
table.insert(offsets, Vector(x, y, z))
tokensPlaced = tokensPlaced + 1
end
end
-- Handle special case for 1 token
if numTokens == 1 then
offsets[1] = Vector(0, 3, -0.2)
end
totalOffsets[numTokens] = offsets
end
return totalOffsets
end
local PLAYER_CARD_TOKEN_OFFSETS = internal.generateOffsets(12)
-- 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 tts__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 playermat 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
-- Spawns a set of tokens on the given card.
---@param card tts__Object Card to spawn tokens on
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@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
---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)
local optionPanel = GlobalApi.getOptionPanelState()
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
-- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.
---@param card tts__Object Card to spawn tokens on
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@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
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)
-- token starts in state 1, so don't attempt to change it to avoid error
if tokenValue ~= 1 then
spawned.setState(tokenValue)
end
end)
end
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
-- Spawns a number of tokens.
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@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 or action tokens
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
local offsets = {}
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
else
-- only up to 12 offset tables defined
if tokenCount > 12 then
printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.")
TokenManager.spawnResourceCounterToken(card, tokenCount)
return
end
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
end
end
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = {}
-- get a vector for the shifting (downwards local to the card)
local shiftDownVector = Vector(0, 0, shiftDown):rotateOver("y", card.getRotation().y)
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset + shiftDownVector
end
end
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
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 or "")]
if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then
callback = function(spawned) spawned.setState(stateID) end
elseif tokenType == "universalActionAbility" then
local matColor = playermatApi.getMatColorByPosition(card.getPosition())
local class = playermatApi.returnInvestigatorClass(matColor)
callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end
end
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 tts__Vector Global position to spawn the token
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@param rotation tts__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
})
end
-- Checks a card for metadata to maybe replenish it
---@param card tts__Object Card object to be replenished
---@param uses table The already decoded metadata.uses (to avoid decoding again)
TokenManager.maybeReplenishCard = function(card, uses)
for _, useInfo in ipairs(uses) do
if useInfo.count and useInfo.replenish then
internal.replenishTokens(card, useInfo)
end
end
end
-- 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
-- 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
-- Checks to see if the given card has location data in the DataHelper
---@param card tts__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
internal.initTokenTemplates = function()
if tokenTemplates ~= nil then
return
end
tokenTemplates = {}
local tokenSource = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSource")
for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do
local tokenName = tokenTemplate.Memo
tokenTemplates[tokenName] = tokenTemplate
end
end
-- Copies the data from the DataHelper. Will only happen once.
internal.initDataHelperData = function()
if playerCardData ~= nil then
return
end
local dataHelper = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper")
playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')
locationData = dataHelper.getTable('LOCATIONS_DATA')
end
-- 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 tts__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 playermat should pass "Charge"=1
internal.spawnTokensFromUses = function(card, extraUses)
local uses = internal.getUses(card)
if uses == nil then return end
-- go through tokens to spawn
local tokenCount
for i, useInfo in ipairs(uses) do
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]
end
-- Shift each spawned group after the first down so they don't pile on each other
TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)
end
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
-- 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 tts__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
-- Spawn tokens for a player card using data retrieved from the Data Helper.
---@param card tts__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)
local token = playerData.tokenType
local tokenCount = playerData.tokenCount
TokenManager.spawnTokenGroup(card, token, tokenCount)
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card tts__Object Card to maybe spawn tokens for
---@param locationData table Location data structure retrieved from the DataHelper. Should be
-- 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
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
end
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
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()
end
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
---@param card tts__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
end
elseif not card.is_face_down then
return metadata.uses
end
return nil
end
-- Dynamically create positions for clues on a card
---@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
internal.buildClueOffsets = function(card, count)
-- make sure clues always spawn from left to right
local modifier = card.is_face_down and 1 or -1
local cluePositions = {}
for i = 1, count do
-- get the set number (1 for clue 1-16, 2 for 17-32 etc.)
local set = math.floor((i - 1) / 16) + 1
-- get the local index (always number from 1-16)
local localIndex = (i - 1) % 16
-- get row and column for this clue
local row = math.floor(localIndex / 4) + 1
local column = localIndex % 4
-- calculate local position
local localPos = Vector((-0.825 + 0.55 * column) * modifier, 0, -1.5 + 0.55 * row)
-- get the global clue position (higher y-position for each set)
local cluePos = card.positionToWorld(localPos) + Vector(0, 0.03 + 0.103 * (set - 1), 0)
-- add position to table
table.insert(cluePositions, cluePos)
end
return cluePositions
end
---@param card tts__Object Card object to be replenished
---@param useInfo table The already decoded subtable of metadata.uses (to avoid decoding again)
internal.replenishTokens = function(card, useInfo)
-- get current amount of matching resource tokens on the card
local clickableResourceCounter = nil
local foundTokens = 0
local maybeDeleteThese = {}
if useInfo.token == "clue" then
for _, obj in ipairs(searchLib.onObject(card, "isClue")) do
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
end
elseif useInfo.token == "doom" then
for _, obj in ipairs(searchLib.onObject(card, "isDoom")) do
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
end
else
-- search for the token instead if there's no special resource state for it
local searchType = string.lower(useInfo.type)
if stateTable[searchType] == nil then
searchType = useInfo.token
end
for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
local memo = obj.getMemo()
if searchType == memo then
foundTokens = foundTokens + math.abs(obj.getQuantity())
table.insert(maybeDeleteThese, obj)
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
end
end
-- this is the theoretical new amount of uses (to be checked below)
local newCount = foundTokens + useInfo.replenish
-- if there are already more uses than the replenish amount, keep them
if foundTokens > useInfo.count then
newCount = foundTokens
-- only replenish up until the replenish amount
elseif newCount > useInfo.count then
newCount = useInfo.count
end
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
-- delete existing tokens
for _, obj in ipairs(maybeDeleteThese) do
obj.destruct()
end
-- spawn new token group
TokenManager.spawnTokenGroup(card, useInfo.token, newCount, _, useInfo.type)
end
end
return TokenManager
end

View File

@ -0,0 +1,67 @@
do
local TokenManagerApi = {}
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable table Key/Value pairs following the DataHelper style
function TokenManagerApi.addLocationData(dataTable)
Global.call("TokenManager.addLocationData", dataTable)
end
-- 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
function TokenManagerApi.addPlayerCardData(dataTable)
Global.call("TokenManager.addPlayerCardData", dataTable)
end
-- 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 tts__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 playermat should pass "Charge"=1
function TokenManagerApi.spawnForCard(card, extraUses)
Global.call("TokenManager.spawnForCard", { card = card, extraUses = extraUses })
end
-- Spawns a single token at the given global position by copying it from the template bag.
---@param position tts__Vector Global position to spawn the token
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@param rotation tts__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
function TokenManagerApi.spawnToken(position, tokenType, rotation, callback)
Global.call("TokenManager.spawnToken", {
position = position,
tokenType = tokenType,
rotation = rotation,
callback = callback
})
end
-- Spawns a set of tokens on the given card.
---@param card tts__Object Card to spawn tokens on
---@param tokenType string Type of token to spawn (template needs to be in source bag)
---@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
---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens
function TokenManagerApi.spawnTokenGroup(card, tokenType, tokenCount, shiftDown, subType)
Global.call("TokenManager.spawnTokenGroup", {
card = card,
tokenType = tokenType,
tokenCount = tokenCount,
shiftDown = shiftDown,
subType = subType
})
end
-- Checks a card for metadata to maybe replenish it
---@param card tts__Object Card object to be replenished
---@param uses table The already decoded metadata.uses (to avoid decoding again)
function TokenManagerApi.maybeReplenishCard(card, uses)
Global.call("TokenManager.maybeReplenishCard", { card = card, uses = uses })
end
return TokenManagerApi
end

View File

@ -1,7 +1,7 @@
require("playercards/CardsWithHelper")
local playermatApi = require("playermat/PlayermatApi")
local searchLib = require("util/SearchLib")
local tokenManager = require("core/token/TokenManager")
local tokenManagerApi = require("core/token/TokenManagerApi")
-- intentionally global
hasXML = true
@ -45,7 +45,7 @@ function add4()
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
tokenManager.spawnTokenGroup(self, "resource", newCount)
tokenManagerApi.spawnTokenGroup(self, "resource", newCount)
end
end

View File

@ -3,7 +3,7 @@ local blessCurseManagerApi = require("chaosbag/BlessCurseManagerApi")
local guidReferenceApi = require("core/GUIDReferenceApi")
local playermatApi = require("playermat/PlayermatApi")
local searchLib = require("util/SearchLib")
local tokenManager = require("core/token/TokenManager")
local tokenManagerApi = require("core/token/TokenManagerApi")
-- intentionally global
hasXML = true
@ -88,7 +88,7 @@ function removeAndExtraAction()
spawned.call("updateClassAndSymbol", { class = "Mystic", symbol = "Mystic" })
spawned.addTag("Temporary")
end
tokenManager.spawnToken(emptyPos + Vector(0, 0.7, 0), "universalActionAbility", rotation, callback)
tokenManagerApi.spawnToken(emptyPos + Vector(0, 0.7, 0), "universalActionAbility", rotation, callback)
end
function elderSignAbility()

View File

@ -6,7 +6,7 @@ local mythosAreaApi = require("core/MythosAreaApi")
local navigationOverlayApi = require("core/NavigationOverlayApi")
local searchLib = require("util/SearchLib")
local tokenChecker = require("core/token/TokenChecker")
local tokenManager = require("core/token/TokenManager")
local tokenManagerApi = require("core/token/TokenManagerApi")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
-- option panel data
@ -361,7 +361,7 @@ function doUpkeep(_, clickedByColor, isRightClick)
-- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile)
if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x > -1 and not obj.is_face_down then
tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)
tokenManagerApi.maybeReplenishCard(obj, cardMetadata.uses, self)
end
elseif obj.type == "Deck" and forcedLearning == false then
-- check decks for forced learning
@ -1103,7 +1103,7 @@ function spawnTokensFor(object)
extraUses["Charge"] = 1
end
tokenManager.spawnForCard(object, extraUses)
tokenManagerApi.spawnForCard(object, extraUses)
end
function onCollisionEnter(collisionInfo)
@ -1243,11 +1243,13 @@ function maybeUpdateActiveInvestigator(card)
-- spawn three regular action tokens (investigator specific one in the bottom spot)
for i = 1, 3 do
local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0))
tokenManager.spawnToken(pos, "universalActionAbility", self.getRotation(),
tokenManagerApi.spawnToken(pos, "universalActionAbility", self.getRotation(),
function(spawned)
spawned.call("updateClassAndSymbol",
{ class = activeInvestigatorData.class, symbol = activeInvestigatorData.class })
{
class = activeInvestigatorData.class,
symbol = activeInvestigatorData.class
})
end)
end
@ -1280,7 +1282,7 @@ function maybeUpdateActiveInvestigator(card)
local localSpawnPos = tokenSpawnPos[type][count[type]]
local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0))
tokenManager.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(),
tokenManagerApi.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(),
function(spawned)
spawned.call("updateClassAndSymbol", { class = activeInvestigatorData.class, symbol = str })
end)
@ -1466,7 +1468,7 @@ function clickableClues(showCounter)
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())
tokenManagerApi.spawnToken(pos, "clue", self.getRotation())
end
end
end
@ -1546,7 +1548,7 @@ end
function updatePlayerCards(args)
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
tokenManagerApi.addPlayerCardData(playerCardData)
end
-- returns the colored steam name or color

View File

@ -1,8 +1,8 @@
local playermatApi = require("playermat/PlayermatApi")
local searchLib = require("util/SearchLib")
local tokenManager = require("core/token/TokenManager")
local TOKEN_INDEX = {}
local tokenManagerApi = require("core/token/TokenManagerApi")
local TOKEN_INDEX = {}
TOKEN_INDEX[1] = "universalActionAbility"
TOKEN_INDEX[3] = "resourceCounter"
TOKEN_INDEX[4] = "damage"
@ -75,7 +75,7 @@ function onScriptingButtonDown(index, playerColor)
end
end
tokenManager.spawnToken(position, tokenType, rotation, callback)
tokenManagerApi.spawnToken(position, tokenType, rotation, callback)
end
-- gets the target card for this operation
@ -128,7 +128,7 @@ function addUseToCard(card, useType)
-- if matching uses were found, perform the "fake" replenish
if match then
tokenManager.maybeReplenishCard(card, metadata.uses)
tokenManagerApi.maybeReplenishCard(card, metadata.uses)
return true
else
return false