diff --git a/src/core/GameKeyHandler.ttslua b/src/core/GameKeyHandler.ttslua index 1cf866e8..4e4e5dcd 100644 --- a/src/core/GameKeyHandler.ttslua +++ b/src/core/GameKeyHandler.ttslua @@ -276,6 +276,12 @@ function removeOneUse(playerColor, hoveredObject) end end + -- error handling + if not targetObject then + broadcastToColor("No tokens found!", playerColor, "Yellow") + return + end + -- release sealed token if card has one and no uses if tokenChecker.isChaosToken(targetObject) and hoveredObject.hasTag("CardThatSeals") then local func = hoveredObject.getVar("releaseOneToken") -- check if function exists @@ -285,12 +291,6 @@ function removeOneUse(playerColor, hoveredObject) end end - -- error handling - if not targetObject then - broadcastToColor("No tokens found!", playerColor, "Yellow") - return - end - -- handling for stacked tokens if targetObject.getQuantity() > 1 then targetObject = targetObject.takeObject() diff --git a/src/core/PlayArea.ttslua b/src/core/PlayArea.ttslua index d86f75c2..18da1f3e 100644 --- a/src/core/PlayArea.ttslua +++ b/src/core/PlayArea.ttslua @@ -464,7 +464,7 @@ end -- Count victory points from locations in play area ---@param highlightOff boolean True if highlighting should be enabled ----@return. Returns the total amount of VP found in the play area +---@return number totalVP Total amount of VP found in the play area function countVP(highlightOff) local totalVP = 0 diff --git a/src/core/token/TokenManager.ttslua b/src/core/token/TokenManager.ttslua index afc89c32..c14c5490 100644 --- a/src/core/token/TokenManager.ttslua +++ b/src/core/token/TokenManager.ttslua @@ -219,9 +219,6 @@ do 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 @@ -250,7 +247,6 @@ do 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 @@ -299,9 +295,10 @@ do ---@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) - -- 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) + for _, useInfo in ipairs(uses) do + if useInfo.count and useInfo.replenish then + internal.replenishTokens(card, useInfo) + end end end @@ -459,59 +456,98 @@ do return nil end - -- Dynamically create positions for clues on a card. + -- 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 - local row = math.floor(1 + (i - 1) / 4) - local column = (i - 1) % 4 - local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row)) - cluePos.y = cluePos.y + 0.05 + -- 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 uses table The already decoded metadata.uses (to avoid decoding again) - internal.replenishTokens = function(card, uses) + ---@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 searchType = string.lower(uses[1].type) - for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do - local memo = obj.getMemo() - - if searchType == memo then + local maybeDeleteThese = {} + if useInfo.token == "clue" then + for _, obj in ipairs(searchLib.onObject(card, "isClue")) do foundTokens = foundTokens + math.abs(obj.getQuantity()) - obj.destruct() - elseif memo == "resourceCounter" then - foundTokens = obj.getVar("val") - clickableResourceCounter = obj - break + 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 + uses[1].replenish + local newCount = foundTokens + useInfo.replenish -- if there are already more uses than the replenish amount, keep them - if foundTokens > uses[1].count then + if foundTokens > useInfo.count then newCount = foundTokens -- only replenish up until the replenish amount - elseif newCount > uses[1].count then - newCount = uses[1].count + 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 - TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type) + -- 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 diff --git a/src/util/SearchLib.ttslua b/src/util/SearchLib.ttslua index 7d5d5fa7..ad5da1b2 100644 --- a/src/util/SearchLib.ttslua +++ b/src/util/SearchLib.ttslua @@ -5,6 +5,7 @@ do 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, + isDoom = function(x) return x.memo == "clueDoom" and x.is_face_down == true end, isTileOrToken = function(x) return x.type == "Tile" end, isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } diff --git a/src/util/TokenSpawnTool.ttslua b/src/util/TokenSpawnTool.ttslua index 650d4dbc..d6be72f4 100644 --- a/src/util/TokenSpawnTool.ttslua +++ b/src/util/TokenSpawnTool.ttslua @@ -1,4 +1,3 @@ -local guidReferenceApi = require("core/GUIDReferenceApi") local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenManager = require("core/token/TokenManager") @@ -24,35 +23,23 @@ function onScriptingButtonDown(index, playerColor) -- check for subtype of resource based on card below if tokenType == "resource" then - local card - local hoverObj = Player[playerColor].getHoverObject() - if hoverObj and hoverObj.type == "Card" then - card = hoverObj - elseif hoverObj then - -- use the first card below the hovered object if it's not a card1 - for _, obj in ipairs(searchLib.belowPosition(position, "isCard")) do - card = obj - break - end + local card = getTargetCard(playerColor, position) + + if card and not card.is_face_down then + local status = addUseToCard(card, tokenType) + if status == true then return end end - -- get the metadata from the card and maybe replenish a use - if card and not card.is_face_down then - local metadata = JSON.decode(card.getGMNotes()) or {} - local uses = metadata.uses or {} - for _, useInfo in ipairs(uses) do - if useInfo.token == "resource" then - -- artifically create replenish data to re-use that existing functionality - uses[1].count = 999 - uses[1].replenish = 1 - local matColor = playermatApi.getMatColorByPosition(position) - local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") - tokenManager.maybeReplenishCard(card, uses, mat) - return - end - end + -- check hovered object for location data or 'uses (x clues)' and add one + elseif tokenType == "clue" then + local card = getTargetCard(playerColor, position) + + if card and (not card.is_face_down or card.hasTag("Location")) then + local status = addUseToCard(card, tokenType) + if status == true then return end end - -- check hovered object for "resourceCounter" tokens and increase them instead + + -- check hovered object for "resourceCounter" tokens and increase them instead elseif tokenType == "resourceCounter" then local hoverObj = Player[playerColor].getHoverObject() if hoverObj then @@ -61,7 +48,8 @@ function onScriptingButtonDown(index, playerColor) return end end - -- check hovered object for "damage" and "horror" tokens and increase them instead + + -- check hovered object for "damage" and "horror" tokens and increase them instead elseif tokenType == "damage" or tokenType == "horror" then local hoverObj = Player[playerColor].getHoverObject() if hoverObj then @@ -74,12 +62,74 @@ function onScriptingButtonDown(index, playerColor) end end end - -- check for nearest investigator card and change action token state to its class + + -- check for nearest investigator card and change action token state to its class elseif tokenType == "universalActionAbility" then local matColor = playermatApi.getMatColorByPosition(position) + local matRotation = playermatApi.returnRotation(matColor) local class = playermatApi.returnInvestigatorClass(matColor) - callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = class }) end + callback = function(spawned) + spawned.setRotation(matRotation) + spawned.call("updateClassAndSymbol", { class = class, symbol = class }) + end end tokenManager.spawnToken(position, tokenType, rotation, callback) end + +-- gets the target card for this operation +---@param playerColor string Color of the triggering player +---@param position tts__Vector Position to check for a card (if there isn't a hovered card) +function getTargetCard(playerColor, position) + local hoverObj = Player[playerColor].getHoverObject() + if hoverObj and hoverObj.type == "Card" then + return hoverObj + elseif hoverObj then + -- use the first card below the hovered object if it's not a card + for _, obj in ipairs(searchLib.belowPosition(position, "isCard")) do + return obj + end + end +end + +-- adds a use to a card (TODO: probably move this to the TokenManager?) +---@param card tts__Object Card that should get a use added +---@param useType string Type of uses to be added +function addUseToCard(card, useType) + local metadata = JSON.decode(card.getGMNotes()) or {} + + -- get correct data for location + if metadata.type == "Location" then + if not card.is_face_down and metadata.locationFront ~= nil then + metadata = metadata.locationFront + elseif metadata.locationBack ~= nil then + metadata = metadata.locationBack + end + + -- if there are no uses at all, add "empty" uses for fake replenishing (only for clues) + if metadata.uses == nil then + metadata.uses = { { token = "clue" } } + end + end + + local match = false + for _, useInfo in ipairs(metadata.uses) do + if useInfo.token == useType then + -- artificially create replenish data to re-use that existing functionality + useInfo.count = 999 + useInfo.replenish = 1 + match = true + else + -- artificially disable other uses from replenishing + useInfo.replenish = nil + end + end + + -- if matching uses were found, perform the "fake" replenish + if match then + tokenManager.maybeReplenishCard(card, metadata.uses) + return true + else + return false + end +end