Code commit for token spawn refactoring

This commit is contained in:
Buhallin 2022-12-28 02:57:43 -08:00
parent ff7ed57bca
commit 7d820601a9
No known key found for this signature in database
GPG Key ID: DB3C362823852294
8 changed files with 728 additions and 272 deletions

View File

@ -202,8 +202,7 @@ end
-- Returns the simple name of a card given its ID. This will find the card and strip any trailing
-- SCED-specific suffixes such as (Taboo) or (Level)
function getCardName(cardId)
local configuration = getConfiguration()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local allCardsBag = getObjectFromGUID("15bb07")
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil) then
local name = card.data.Nickname

View File

@ -1,3 +1,5 @@
local tokenManager = require("core/token/TokenManager")
---------------------------------------------------------
-- general setup
---------------------------------------------------------
@ -331,32 +333,11 @@ end
-- token spawning
---------------------------------------------------------
-- DEPRECATED. Use TokenManager instead.
-- Spawns a single token.
---@param params Table. Array with arguments to the method. 1 = position, 2 = type, 3 = rotation
function spawnToken(params)
local position = params[1]
local tokenType = params[2]
local rotation = params[3] or {0, 270, 0}
local tokenData = TOKEN_DATA[tokenType]
if tokenData == nil then
error("no token data found for '" .. tokenType .. "'")
end
local token = spawnObject({
type = 'Custom_Token',
position = position,
rotation = rotation
})
token.setCustomObject({
image = tokenData['image'],
thickness = 0.3,
merge_distance = 5,
stackable = true
})
token.use_snap_points = false
token.scale(tokenData['scale'])
return token
return tokenManager.spawnToken(params[1], params[2], params[3])
end
---------------------------------------------------------
@ -614,7 +595,7 @@ end
-- Content Importing and XML functions
---------------------------------------------------------
local source_repo = 'https://raw.githubusercontent.com/seth-sced/loadable-objects/main'
local source_repo = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'
local library = nil
local request_obj
@ -640,7 +621,7 @@ end
function onClick_toggleUi(_, title)
UI.hide('optionPanel')
UI.hide('load_ui')
-- when same button is clicked or close window button is pressed, don't open UI
if UI.getValue('title') ~= title and title ~= 'Hidden' then
UI.setValue('title', title)
@ -836,7 +817,7 @@ function applyOptionPanelChange(id, state)
-- update master clue counter
getObjectFromGUID("4a3aa4").setVar("useClickableCounters", state)
-- option: Show token arranger
elseif id == "showTokenArranger" then
-- delete previously pulled out tokens
@ -847,7 +828,7 @@ function applyOptionPanelChange(id, state)
-- option: Show clean up helper
elseif id == "showCleanUpHelper" then
optionPanel[id] = spawnOrRemoveHelper(state, "Clean Up Helper", {-68, 1.6, 35.5})
-- option: Show hand helper for each player
elseif id == "showHandHelper" then
optionPanel[id][1] = spawnOrRemoveHelper(state, "Hand Helper", {-50.84, 1.6, 7.02}, {0, 270, 0}, "White")
@ -928,7 +909,7 @@ function removeHelperObject(name)
["Chaos Bag Manager"] = "showChaosBagManager",
["jaqenZann's Navigation Overlay"] = "showNavigationOverlay"
}
local data = optionPanel[referenceTable[name]]
-- if there is a GUID stored, remove that object

View File

@ -1,4 +1,14 @@
local playAreaApi = require("core/PlayAreaApi")
local tokenSpawnTracker = require("core/token/TokenSpawnTrackerApi")
local ENCOUNTER_DECK_AREA = {
upperLeft = { x = 0.9, z = 0.42 },
lowerRight = { x = 0.86, z = 0.38 },
}
local ENCOUNTER_DISCARD_AREA = {
upperLeft = { x = 1.62, z = 0.42 },
lowerRight = { x = 1.58, z = 0.38 },
}
local currentScenario
@ -15,8 +25,7 @@ function onSave()
})
end
-- TTS event handler. Checks for a scenrio card, extracts the scenario name from the description,
-- and fires it to the relevant listeners.
-- TTS event handler. Handles scenario name event triggering and encounter card token resets.
function onCollisionEnter(collisionInfo)
local object = collisionInfo.collision_object
if object.getName() == "Scenario" then
@ -25,9 +34,38 @@ function onCollisionEnter(collisionInfo)
fireScenarioChangedEvent()
end
end
local localPos = self.positionToLocal(object.getPosition())
if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then
tokenSpawnTracker.resetTokensSpawned(object.getGUID())
end
end
-- Listens for cards entering the encounter deck or encounter discard, and resets the spawn state
-- for the cards when they do.
function onObjectEnterContainer(container, object)
Wait.frames(function() resetTokensIfInDeckZone(container, object) end, 1)
end
function resetTokensIfInDeckZone(container, object)
local localPos = self.positionToLocal(container.getPosition())
if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then
tokenSpawnTracker.resetTokensSpawned(object.getGUID())
end
end
function fireScenarioChangedEvent()
log("Firing")
playAreaApi.onScenarioChanged(currentScenario)
end
-- 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

View File

@ -1,3 +1,4 @@
local tokenManager = require("core/token/TokenManager")
---------------------------------------------------------
-- general setup
---------------------------------------------------------
@ -23,8 +24,6 @@ local SHIFT_EXCLUSION = {
local INVESTIGATOR_COUNTER_GUID = "f182ee"
local PLAY_AREA_ZONE_GUID = "a2f932"
local clueData = {}
spawnedLocationGUIDs = {}
local currentScenario
---------------------------------------------------------
@ -33,7 +32,6 @@ local currentScenario
function onSave()
return JSON.encode({
spawnedLocs = spawnedLocationGUIDs,
currentScenario = currentScenario
})
end
@ -41,21 +39,8 @@ end
function onLoad(saveState)
-- records locations we have spawned clues for
local saveData = JSON.decode(saveState) or {}
spawnedLocationGUIDs = saveData.spawnedLocs or { }
currentScenario = saveData.currentScenario
local TOKEN_DATA = Global.getTable('TOKEN_DATA')
clueData = {
thickness = 0.1,
stackable = true,
type = 2,
image = TOKEN_DATA.clue.image,
image_bottom = TOKEN_DATA.doom.image
}
local dataHelper = getObjectFromGUID('708279')
LOCATIONS = dataHelper.getTable('LOCATIONS_DATA')
self.interactable = DEBUG
Wait.time(function() COLLISION_ENABLED = true end, 1)
end
@ -64,72 +49,13 @@ function log(message)
if DEBUG then print(message) end
end
---------------------------------------------------------
-- clue spawning
---------------------------------------------------------
-- try the compound key then the name alone as default
function getLocation(object)
return LOCATIONS[object.getName() .. '_' .. object.getGUID()] or LOCATIONS[object.getName()]
end
-- Return the number of clues to spawn on this location
function getClueCount(object, isFaceDown, playerCount)
local details = getLocation(object)
if details == nil then
error('attempted to get clue for unexpected object: ' .. object.getName())
end
log(object.getName() .. ' : ' .. details['type'] .. ' : ' .. details['value'] .. ' : ' .. details['clueSide'])
if ((isFaceDown and details['clueSide'] == 'back') or (not isFaceDown and details['clueSide'] == 'front')) then
if details['type'] == 'fixed' then
return details['value']
elseif details['type'] == 'perPlayer' then
return details['value'] * playerCount
end
error('unexpected location type: ' .. details['type'])
end
return 0
end
function spawnCluesAtLocation(clueCount, object)
if spawnedLocationGUIDs[object.getGUID()] ~= nil then
error('tried to spawn clue for already spawned location:' .. object.getName())
end
log('spawning clues for ' .. object.getName() .. '_' .. object.getGUID())
log('player count is ' .. getInvestigatorCount() .. ', clue count is ' .. clueCount)
-- mark this location as spawned, can't happen again
spawnedLocationGUIDs[object.getGUID()] = true
-- spawn clues (starting top right, moving to the next row after 4 clues)
local pos = object.getPosition()
for i = 1, clueCount do
local row = math.floor(1 + (i - 1) / 4)
local column = (i - 1) % 4
spawnClue({ pos.x + 1.5 - 0.55 * row, pos.y, pos.z - 0.825 + 0.55 * column })
end
end
function spawnClue(position)
local token = spawnObject({
position = position,
rotation = { 3.88, -90, 0.24 },
type = 'Custom_Tile'
})
token.setCustomObject(clueData)
token.scale { 0.25, 1, 0.25 }
token.use_snap_points = false
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
function updateLocations(args)
custom_data_helper_guid = args[1]
local custom_data_helper = getObjectFromGUID(args[1])
for k, v in pairs(custom_data_helper.getTable("LOCATIONS_DATA")) do
LOCATIONS[k] = v
local customDataHelper = getObjectFromGUID(args[1])
if customDataHelper ~= nil then
tokenManager.addLocationData(customDataHelper.getTable("LOCATIONS_DATA"))
end
end
@ -137,13 +63,23 @@ function onCollisionEnter(collision_info)
if not COLLISION_ENABLED then return end
-- check if we should spawn clues here and do so according to playercount
local object = collision_info.collision_object
if getLocation(object) ~= nil and spawnedLocationGUIDs[object.getGUID()] == nil then
local clueCount = getClueCount(object, object.is_face_down, getInvestigatorCount())
if clueCount > 0 then spawnCluesAtLocation(clueCount, object) end
local card = collision_info.collision_object
if shouldSpawnTokens(card) then
tokenManager.spawnForCard(card)
end
end
function shouldSpawnTokens(card)
local metadata = JSON.decode(card.getGMNotes())
if metadata == nil then
return tokenManager.hasLocationData(card) ~= nil
end
return metadata.type == "Location"
or metadata.type == "Enemy"
or metadata.type == "Treachery"
or metadata.weakness
end
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
-- fixed objects will be ignored, as will anything the player has tagged with
-- 'displacement_excluded'

View File

@ -0,0 +1,446 @@
do
local tokenSpawnTracker = require("core/token/TokenSpawnTrackerApi")
local playAreaApi = require("core/PlayAreaApi")
local PLAYER_CARD_TOKEN_OFFSETS = {
[1] = {
Vector(0, 8, -0.2)
},
[2] = {
Vector(0.4, 20, -0.2),
Vector(-0.4, 20, -0.2)
},
[3] = {
Vector(0, 8, -0.9),
Vector(0.4, 8, -0.2),
Vector(-0.4, 8, -0.2)
},
[4] = {
Vector(0.4, 10, -0.9),
Vector(-0.4, 10, -0.9),
Vector(0.4, 10, -0.2),
Vector(-0.4, 10, -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)
}
}
local SOURCE_BAG_GUIDS = {
damage = "480bda",
horror = "c3ecf4",
resource = "9fadf9",
doom = "47ffc3",
clue = "31fa39",
}
local DATA_HELPER_GUID = "708279"
local playerCardData
local locationData
local TokenManager = { }
local internal = { }
-- Spawns tokens for the card. This function is built to just throw a card at it and let it do
-- the work. 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 tokenSpawnTracker.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 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 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 An offset for the z-value of this group of tokens
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown)
if tokenType == "damage" or tokenType == "horror" then
TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
else
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown)
end
end
-- 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 Value to set the damage/horror to
---@param shiftDown An offset for the z-value of this group of tokens
TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
if tokenCount < 1 or tokenCount > 50 then
return
end
local offsets = PLAYER_CARD_TOKEN_OFFSETS[1]
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = { }
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset
offsets[i][3] = offsets[i][3] + shiftDown
end
end
local pos = card.positionToWorld(offsets[1])
pos.y = card.getPosition().y + 0.15
TokenManager.spawnToken(pos, tokenType, card.getRotation(), function(spawned)
spawned.setState(tokenValue)
end)
end
-- 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 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 An offset for the z-value of this group of tokens
TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown)
if tokenCount < 1 or tokenCount > 12 then
return
end
local offsets
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
else
offsets = { }
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
-- end
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = { }
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset
offsets[i][3] = offsets[i][3] + shiftDown
end
end
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
end
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation())
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 A callback function triggered after the new token is spawned
TokenManager.spawnToken = function(position, tokenType, rotation, callback)
if SOURCE_BAG_GUIDS[tokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local sourceBag = getObjectFromGUID(SOURCE_BAG_GUIDS[tokenType])
if sourceBag == nil then
error("No token source for '" .. tokenType .. "'")
return
end
-- All the source bags are infinite, so we just grab the first object
local tokenTemplate = sourceBag.getData().ContainedObjects[1]
-- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
local tokenRotation = rotation or { x = 0, y = 270, z = 0 }
tokenTemplate.Transform.rotY = tokenRotation.y
log("x=" .. tokenTemplate.Transform.rotX .. ",y=" .. tokenTemplate.Transform.rotY .. "z=" .. tokenTemplate.Transform.rotZ)
return spawnObjectData({
data = tokenTemplate,
position = position,
callback_function = callback
})
end
-- 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)
tokenSpawnTracker.resetTokensSpawned(card.getGUID())
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 Object Card to check for data
---@return Boolean True if this card has data in the helper, false otherwise
TokenManager.hasLocationData = function(card)
return internal.getLocationData(card) ~= nil
end
-- Copies the data from the DataHelper. Will only happen once.
internal.initDataHelperData = function()
if playerCardData ~= nil then
return
end
local dataHelper = getObjectFromGUID(DATA_HELPER_GUID)
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 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
local type = nil
local token = nil
local tokenCount = 0
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. This is ugly and duplicated, but impossible to replicate the
-- multi-spawn vs. single spawn otherwise. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if uses.count != nil then
type = cardMetadata.uses.type
token = cardMetadata.uses.token
tokenCount = cardMetadata.uses.count
if extraUses ~= nil and extraUses[type] ~= nil then
tokenCount = tokenCount + extraUses[type]
end
log("Spawning single use tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
TokenManager.spawnTokenGroup(card, token, tokenCount)
else
for i, useInfo in ipairs(uses) do
type = useInfo.type
token = useInfo.token
tokenCount = (useInfo.count or 0)
+ (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()
if extraUses ~= nil and extraUses[type] ~= nil then
tokenCount = tokenCount + extraUses[type]
end
log("Spawning use array tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
-- Shift each spawned group after the first down so they don't pile on each other
TokenManager.spawnTokenGroup(card, token, tokenCount, (i - 1) * 0.6)
end
end
tokenSpawnTracker.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 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 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)
token = playerData.tokenType
tokenCount = playerData.tokenCount
log("Spawning data helper tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
TokenManager.spawnTokenGroup(card, token, tokenCount)
tokenSpawnTracker.markTokensSpawned(card.getGUID())
end
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
---@param playerData 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)
tokenSpawnTracker.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
log(card.getName() .. ' : ' .. locationData.type .. ' : ' .. locationData.value .. ' : ' .. locationData.clueSide)
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 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 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
local row = math.floor(1 + (i - 1) / 4)
local column = (i - 1) % 4
table.insert(cluePositions, Vector(pos.x + 1.5 - 0.55 * row, pos.y + 0.15, pos.z - 0.825 + 0.55 * column))
end
return cluePositions
end
return TokenManager
end

View File

@ -0,0 +1,85 @@
local spawnedCardGuids = { }
local HAND_ZONES = { }
HAND_ZONES["c506bf"] = true -- White
HAND_ZONES["67ce9a"] = true -- Green
HAND_ZONES["cbc751"] = true -- Orange
HAND_ZONES["57c22c"] = true -- Red
function onLoad(saveState)
if saveState ~= nil then
local saveTable = JSON.decode(saveState) or { }
spawnedCardGuids = saveTable.cards or { }
end
createResetMenuItems()
end
function onSave()
return JSON.encode({
cards = spawnedCardGuids
})
end
function createResetMenuItems()
self.addContextMenuItem("Reset All", resetAll)
self.addContextMenuItem("Reset Locations", resetAllLocations)
self.addContextMenuItem("Reset Player Cards", resetAllAssetAndEvents)
end
function hasSpawnedTokens(cardGuid)
return spawnedCardGuids[cardGuid] == true
end
function markTokensSpawned(cardGuid)
spawnedCardGuids[cardGuid] = true
end
function resetTokensSpawned(cardGuid)
spawnedCardGuids[cardGuid] = nil
end
function resetAllAssetAndEvents()
local resetList = { }
for cardGuid, _ in pairs(spawnedCardGuids) do
local card = getObjectFromGUID(cardGuid)
if card ~= nil then
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
-- Check this by type rather than the PlayerCard tag so we don't reset weaknesses
if cardMetadata.type == "Asset" or cardMetadata.type == "Event" then
resetList[cardGuid] = true
end
end
end
for cardGuid, _ in pairs(resetList) do
spawnedCardGuids[cardGuid] = nil
end
end
function resetAllLocations()
local resetList = { }
for cardGuid, _ in pairs(spawnedCardGuids) do
local card = getObjectFromGUID(cardGuid)
if card ~= nil then
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
-- Check this by type rather than the PlayerCard tag so we don't reset weaknesses
if cardMetadata.type == "Location" then
resetList[cardGuid] = true
end
end
end
for cardGuid, _ in pairs(resetList) do
spawnedCardGuids[cardGuid] = nil
end
end
function resetAll()
spawnedCardGuids = { }
end
-- Listener to reset card token spawns when they enter a hand.
function onObjectEnterZone(zone, enterObject)
if HAND_ZONES[zone.getGUID()] then
resetTokensSpawned(enterObject.getGUID())
end
end

View File

@ -0,0 +1,31 @@
do
local TokenSpawnTracker = { }
local SPAWN_TRACKER_GUID = "e3ffc9"
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("hasSpawnedTokens", cardGuid)
end
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("markTokensSpawned", cardGuid)
end
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetTokensSpawned", cardGuid)
end
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllAssetAndEvents")
end
TokenSpawnTracker.resetAllLocations = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllLocations")
end
TokenSpawnTracker.resetAll = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAll")
end
return TokenSpawnTracker
end

View File

@ -1,5 +1,7 @@
local tokenManager = require("core/token/TokenManager")
-- set true to enable debug logging and show Physics.cast()
local DEBUG = false
local DEBUG = true
-- we use this to turn off collision handling until onLoad() is complete
local COLLISION_ENABLED = false
@ -36,6 +38,16 @@ local INVESTIGATOR_AREA = {
z = -0.0805,
}
}
local THREAT_AREA = {
upperLeft = {
x = 1.53,
z = -0.34
},
lowerRight = {
x = -1.13,
z = -0.92,
}
}
local PLAY_ZONE_ROTATION = self.getRotation()
@ -58,9 +70,6 @@ end
function onLoad(save_state)
self.interactable = DEBUG
DATA_HELPER = getObjectFromGUID('708279')
PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA')
PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS')
TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
@ -387,38 +396,6 @@ function shuffleDiscardIntoDeck()
discardPile = nil
end
function spawnTokenOn(object, offsets, tokenType)
local tokenPosition = object.positionToWorld(offsets)
spawnToken(tokenPosition, tokenType)
end
-- Spawn a group of tokens of the given type on the object
-- @param object Object to spawn the tokens on
-- @param tokenType Type of token to be spawned
-- @param tokenCount Number of tokens to spawn
-- @param shiftDown Amount to shift this group down to avoid spawning multiple token groups on
-- top of each other. Negative values are allowed, and will move the group up instead. This is
-- a static value and is unaware of how many tokens were spawned previously; callers should
-- ensure the proper shift.
function spawnTokenGroup(object, tokenType, tokenCount, shiftDown)
if (tokenCount < 1 or tokenCount > 12) then return end
local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = { }
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset
offsets[i][3] = offsets[i][3] + shiftDown
end
end
if offsets == nil then error("couldn't find offsets for " .. tokenCount .. ' tokens') end
for i = 1, tokenCount do
spawnTokenOn(object, offsets[i], tokenType)
end
end
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
@ -457,133 +434,102 @@ function replenishTokens(card, count, replenish)
local newCount = foundTokens + replenish
if newCount > count then newCount = count end
spawnTokenGroup(card, "resource", newCount)
tokenManager.spawnTokenGroup(card, "resource", newCount)
end
function getPlayerCardData(object)
return PLAYER_CARDS[object.getName()..':'..object.getDescription()] or PLAYER_CARDS[object.getName()]
end
function shouldSpawnTokens(object)
-- don't spawn tokens if in doubt, this should only ever happen onLoad and prevents respawns
local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
local hasDataHelperData = getPlayerCardData(object)
local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local hasUses = cardMetadata.uses ~= nil
return not spawned and (hasDataHelperData or hasUses)
end
function markSpawned(object)
local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true})
if not saved then error('attempt to mark player card spawned before data loaded') end
end
-- contains position and amount of boxes for the upgradesheets that change uses
-- Alchemical Distillation, Damning Testimony, Living Ink and Hyperphysical Shotcaster
local customizationsTable = {
["09040"] = {5, 2},
["09059"] = {2, 2},
["09079"] = {3, 2},
["09119"] = {6, 4}
}
function spawnTokensFor(object)
local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local type = nil
local token = nil
local tokenCount = 0
if cardMetadata.uses ~= nil then
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. This is ugly and duplicated, but impossible to replicate the
-- multi-spawn vs. single spawn otherwise. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if cardMetadata.uses.count != nil then
type = cardMetadata.uses.type
token = cardMetadata.uses.token
tokenCount = cardMetadata.uses.count
if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
else
for i, useInfo in ipairs(cardMetadata.uses) do
type = useInfo.type
token = useInfo.token
tokenCount = useInfo.count
-- additional uses for certain customizable cards (by checking the upgradesheets)
if customizationsTable[cardMetadata.id] ~= nil then
for _, obj in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = obj.hit_object
if obj.tag == "Card" then
local notes = JSON.decode(obj.getGMNotes()) or {}
if notes.id == (cardMetadata.id .. "-c") then
local pos = customizationsTable[cardMetadata.id][1]
local boxes = customizationsTable[cardMetadata.id][2]
if obj.getVar("markedBoxes")[pos] == boxes then tokenCount = tokenCount + 2 end
break
end
end
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
if cardMetadata ~= nil and cardMetadata.customizations ~= nil then
for _, obj in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = obj.hit_object
local notes = JSON.decode(obj.getGMNotes()) or { }
if notes.id == (cardMetadata.id .. "-c") then
for i, customization in ipairs(cardMetadata.customizations) do
if obj.getVar("markedBoxes")[i] == customization.xp
and customization.replaces ~= nil
and customization.replaces.uses ~= nil then
cardMetadata.uses = customization.replaces.uses
card.setGMNotes(JSON.encode(cardMetadata))
end
end
-- additional charge for Akachi
if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
-- Shift each spawned group after the first down so they don't pile on each other
spawnTokenGroup(object, token, tokenCount, (i - 1) * 0.6)
end
end
else
local data = getPlayerCardData(object)
token = data['tokenType']
tokenCount = data['tokenCount']
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
end
markSpawned(object)
end
function resetSpawnState()
local zone = getObjectFromGUID(zoneID)
if zone == nil then return end
for _, object in ipairs(zone.getObjects()) do
if object.tag == "Card" then
unmarkSpawned(object.getGUID(), true)
elseif object.tag == "Deck" then
local cards = object.getObjects()
for _, v in ipairs(cards) do
unmarkSpawned(v.guid)
end
end
end
end
function unmarkSpawned(guid, force)
if not force and getObjectFromGUID(guid) ~= nil then return end
DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false})
function spawnTokensFor(object)
local extraUses = { }
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
end
tokenManager.spawnForCard(object, extraUses)
end
function onCollisionEnter(collision_info)
if not COLLISION_ENABLED then return end
Wait.time(resetSpawnState, 1)
local object = collision_info.collision_object
-- only continue for cards
if object.name ~= "Card" and object.name ~= "CardCustom" then return end
maybeUpdateActiveInvestigator(object)
-- don't spawn tokens for cards in discard pile / threat area
local localpos = self.positionToLocal(object.getPosition())
if localpos.x < -0.7 or localpos.z < -0.3 then
log('Not spawning tokens, relative coordinates are x: ' .. localpos.x .. ' z: ' .. localpos.z)
elseif not object.is_face_down and shouldSpawnTokens(object) then
syncCustomizableMetadata(object)
if isInDeckZone(object) then
tokenManager.resetTokensSpawned(object)
elseif shouldSpawnTokens(object) then
spawnTokensFor(object)
end
end
function shouldSpawnTokens(card)
if card.is_face_down then
return false
end
local localCardPos = self.positionToLocal(card.getPosition())
local metadata = JSON.decode(card.getGMNotes())
-- 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
-- Spawn tokens for assets and events on the main area, and all encounter types in the threat area
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
or metadata.type == "Enemy"
or metadata.weakness) then
return true
end
return false
end
function onObjectEnterContainer(container, object)
Wait.frames(function() resetTokensIfInDeckZone(container, object) end, 1)
end
function resetTokensIfInDeckZone(container, object)
if isInDeckZone(container) then
tokenManager.resetTokensSpawned(object)
end
end
function isInDeckZone(checkObject)
local deckZone = getObjectFromGUID(zoneID)
if deckZone == nil then
return false
end
for _, obj in ipairs(deckZone.getObjects()) do
if obj == checkObject then
return true
end
end
return false
end
---------------------------------------------------------
-- investigator ID grabbing and stat tracker
---------------------------------------------------------
@ -655,10 +601,6 @@ function drawEncountercard(_, _, isRightClick)
Global.call("drawEncountercard", {self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET), self.getRotation(), isRightClick})
end
function spawnToken(position, tokenType)
Global.call('spawnToken', {position, tokenType, PLAY_ZONE_ROTATION})
end
-- Sets this playermat's draw 1 button to visible
---@param visible Boolean. Whether the draw 1 button should be visible
function showDrawButton(visible)
@ -680,7 +622,7 @@ function showDrawButton(visible)
-- remove the "Draw 1" button
else
local buttons = self.getButtons()
for i = 1, #buttons do
for i = 1, #buttons do
if buttons[i].label == "Draw 1" then
self.removeButton(buttons[i].index)
end
@ -721,7 +663,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
spawnToken(pos, "clue")
TokenManager.spawnToken(pos, "clue", self.getRotation())
end
end
end
@ -802,9 +744,7 @@ end
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
local custom_data_helper = getObjectFromGUID(args[1])
data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA")
for k, v in pairs(data_player_cards) do
PLAYER_CARDS[k] = v
end
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
end