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

2249 lines
77 KiB
Plaintext
Raw Normal View History

2022-12-13 14:02:30 -05:00
-- Bundled by luabundle {"version":"1.6.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
local loadingPlaceholder = {[{}] = true}
local register
local modules = {}
local require
local loaded = {}
register = function(name, body)
if not modules[name] then
modules[name] = body
end
end
require = function(name)
local loadedModule = loaded[name]
if loadedModule then
if loadedModule == loadingPlaceholder then
return nil
end
else
if not modules[name] then
if not superRequire then
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
error('Tried to require ' .. identifier .. ', but no such module has been registered')
else
return superRequire(name)
end
end
loaded[name] = loadingPlaceholder
loadedModule = modules[name](require, loaded, register, modules)
loaded[name] = loadedModule
end
return loadedModule
end
return require, loaded, register, modules
end)(nil)
2024-03-10 09:56:22 -04:00
__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-01-06 21:32:29 -05:00
do
2024-03-10 09:56:22 -04:00
local ChaosBagApi = {}
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- respawns the chaos bag with a new state of tokens
---@param tokenList table List of chaos token ids
ChaosBagApi.setChaosBagState = function(tokenList)
return Global.call("setChaosBagState", tokenList)
end
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- returns a Table List of chaos token ids in the current chaos bag
-- requires copying the data into a new table because TTS is weird about handling table return values in Global
ChaosBagApi.getChaosBagState = function()
local chaosBagContentsCatcher = Global.call("getChaosBagState")
local chaosBagContents = {}
for _, v in ipairs(chaosBagContentsCatcher) do
table.insert(chaosBagContents, v)
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
return chaosBagContents
2024-01-06 21:32:29 -05:00
end
2024-03-10 09:56:22 -04:00
-- checks scripting zone for chaos bag (also called by a lot of objects!)
ChaosBagApi.findChaosBag = function()
return Global.call("findChaosBag")
end
-- returns a table of object references to the tokens in play (does not include sealed tokens!)
ChaosBagApi.getTokensInPlay = function()
return Global.call("getChaosTokensinPlay")
end
-- returns all sealed tokens on cards to the chaos bag
---@param playerColor string Color of the player to show the broadcast to
ChaosBagApi.releaseAllSealedTokens = function(playerColor)
return Global.call("releaseAllSealedTokens", playerColor)
end
-- returns all drawn tokens to the chaos bag
ChaosBagApi.returnChaosTokens = function()
return Global.call("returnChaosTokens")
end
-- removes the specified chaos token from the chaos bag
---@param id string ID of the chaos token
ChaosBagApi.removeChaosToken = function(id)
return Global.call("removeChaosToken", id)
end
-- returns a chaos token to the bag and calls all relevant functions
---@param token tts__Object Chaos token to return
ChaosBagApi.returnChaosTokenToBag = function(token)
return Global.call("returnChaosTokenToBag", token)
end
-- spawns the specified chaos token and puts it into the chaos bag
---@param id string ID of the chaos token
ChaosBagApi.spawnChaosToken = function(id)
return Global.call("spawnChaosToken", id)
end
-- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens
-- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the
-- contents of the bag should check this method before doing so.
-- This method will broadcast a message to all players if the bag is being searched.
---@return any canTouch True if the bag is manipulated, false if it should be blocked.
ChaosBagApi.canTouchChaosTokens = function()
return Global.call("canTouchChaosTokens")
end
-- called by playermats (by the "Draw chaos token" button)
---@param mat tts__Object Playermat that triggered this
---@param drawAdditional boolean Controls whether additional tokens should be drawn
---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag
---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag
2024-06-09 10:10:21 -04:00
---@param returnedToken? tts__Object Token to be replaced with newly drawn token
ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)
return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})
2024-03-10 09:56:22 -04:00
end
-- returns a Table List of chaos token ids in the current chaos bag
-- requires copying the data into a new table because TTS is weird about handling table return values in Global
ChaosBagApi.getIdUrlMap = function()
return Global.getTable("ID_URL_MAP")
end
return ChaosBagApi
2024-01-06 21:32:07 -05:00
end
end)
2024-03-10 09:56:22 -04:00
__bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local NavigationOverlayApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
local function getNOHandler()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "NavigationOverlayHandler")
end
-- copies the visibility for the Navigation overlay
---@param startColor string Color of the player to copy from
---@param targetColor string Color of the targeted player
NavigationOverlayApi.copyVisibility = function(startColor, targetColor)
getNOHandler().call("copyVisibility", {
startColor = startColor,
targetColor = targetColor
})
end
-- changes the Navigation Overlay view ("Full View" --> "Play Areas" --> "Closed" etc.)
---@param playerColor string Color of the player to update the visibility for
NavigationOverlayApi.cycleVisibility = function(playerColor)
getNOHandler().call("cycleVisibility", playerColor)
end
-- loads the specified camera for a player
---@param player tts__Player Player whose camera should be moved
---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to
NavigationOverlayApi.loadCamera = function(player, camera)
getNOHandler().call("loadCameraFromApi", {
player = player,
camera = camera
})
end
return NavigationOverlayApi
2024-02-17 19:48:30 -05:00
end
end)
2024-06-09 10:10:21 -04:00
__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-17 19:48:30 -05:00
do
2024-06-09 10:10:21 -04:00
local DeckLib = {}
local searchLib = require("util/SearchLib")
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- places a card/deck at a position or merges into an existing deck
---@param obj tts__Object Object to move
---@param pos table New position for the object
---@param rot? table New rotation for the object
---@param below? boolean Should the object be placed below an existing deck?
DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)
if obj == nil or pos == nil then return end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- search the new position for existing card/deck
local searchResult = searchLib.atPosition(pos, "isCardOrDeck")
-- get new position
local offset = 0.5
local newPos = Vector(pos) + Vector(0, offset, 0)
if #searchResult == 1 then
local bounds = searchResult[1].getBounds()
if below then
newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2)
else
newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset)
end
end
-- allow moving the objects smoothly out of the hand
obj.use_hands = false
if rot then
obj.setRotationSmooth(rot, false, true)
end
obj.setPositionSmooth(newPos, false, true)
-- continue if the card stops smooth moving
Wait.condition(
function()
obj.use_hands = true
-- this avoids a TTS bug that merges unrelated cards that are not resting
if #searchResult == 1 and searchResult[1] ~= obj then
-- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)
pcall(function() searchResult[1].putObject(obj) end)
end
end,
function() return not obj.isSmoothMoving() end, 3)
2024-01-06 21:32:07 -05:00
end
2024-06-09 10:10:21 -04:00
return DeckLib
2024-02-17 19:48:30 -05:00
end
end)
2024-06-09 10:10:21 -04:00
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-03-10 09:56:22 -04:00
do
2024-06-09 10:10:21 -04:00
local TokenSpawnTracker = {}
2024-03-10 09:56:22 -04:00
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-01-06 21:32:29 -05:00
2024-06-09 10:10:21 -04:00
local function getSpawnTracker()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker")
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getSpawnTracker().call("hasSpawnedTokens", cardGuid)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getSpawnTracker().call("markTokensSpawned", cardGuid)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getSpawnTracker().call("resetTokensSpawned", cardGuid)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getSpawnTracker().call("resetAllAssetAndEvents")
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetAllLocations = function()
return getSpawnTracker().call("resetAllLocations")
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetAll = function()
return getSpawnTracker().call("resetAll")
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
return TokenSpawnTracker
end
end)
__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local SearchLib = {}
local filterFunctions = {
isActionToken = function(x) return x.getDescription() == "Action Token" end,
isCard = function(x) return x.type == "Card" end,
isDeck = function(x) return x.type == "Deck" end,
isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end,
isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end,
isTileOrToken = function(x) return x.type == "Tile" end
}
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- performs the actual search and returns a filtered list of object references
---@param pos tts__Vector Global position
---@param rot? tts__Vector Global rotation
---@param size table Size
---@param filter? string Name of the filter function
---@param direction? table Direction (positive is up)
---@param maxDistance? number Distance for the cast
local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)
local filterFunc
if filter then
filterFunc = filterFunctions[filter]
end
local searchResult = Physics.cast({
origin = pos,
direction = direction or { 0, 1, 0 },
orientation = rot or { 0, 0, 0 },
type = 3,
size = size,
max_distance = maxDistance or 0
})
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- filtering the result
local objList = {}
for _, v in ipairs(searchResult) do
if not filter or filterFunc(v.hit_object) then
table.insert(objList, v.hit_object)
end
end
return objList
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- searches the specified area
SearchLib.inArea = function(pos, rot, size, filter)
return returnSearchResult(pos, rot, size, filter)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- searches the area on an object
SearchLib.onObject = function(obj, filter)
pos = obj.getPosition()
size = obj.getBounds().size:setAt("y", 1)
return returnSearchResult(pos, _, size, filter)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- searches the specified position (a single point)
SearchLib.atPosition = function(pos, filter)
size = { 0.1, 2, 0.1 }
return returnSearchResult(pos, _, size, filter)
2024-01-06 21:32:07 -05:00
end
2024-06-09 10:10:21 -04:00
-- searches below the specified position (downwards until y = 0)
SearchLib.belowPosition = function(pos, filter)
direction = { 0, -1, 0 }
maxDistance = pos.y
return returnSearchResult(pos, _, size, filter, direction, maxDistance)
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
return SearchLib
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end)
2024-06-09 10:10:21 -04:00
__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-03-10 09:56:22 -04:00
do
2024-06-09 10:10:21 -04:00
local OptionPanelApi = {}
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- loads saved options
---@param options table Set a new state for the option table
OptionPanelApi.loadSettings = function(options)
return Global.call("loadSettings", options)
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
---@return any: Table of option panel state
OptionPanelApi.getOptions = function()
return Global.getTable("optionPanel")
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
return OptionPanelApi
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end)
2024-06-09 10:10:21 -04:00
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("playermat/Playmat")
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
local chaosBagApi = require("chaosbag/ChaosBagApi")
local deckLib = require("util/DeckLib")
local guidReferenceApi = require("core/GUIDReferenceApi")
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")
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- we use this to turn off collision handling until onLoad() is complete
local collisionEnabled = false
local currentlyEditingSlots = false
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- x-Values for discard buttons
local DISCARD_BUTTON_X_START = -1.365
local DISCARD_BUTTON_X_OFFSET = 0.455
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
local SEARCH_AROUND_SELF_X_BUFFER = 8
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- defined areas for object searching
local MAIN_PLAY_AREA = {
upperLeft = { x = 1.98, z = 0.736 },
lowerRight = { x = -0.79, z = -0.39 }
}
local INVESTIGATOR_AREA = {
upperLeft = { x = -1.084, z = 0.06517 },
lowerRight = { x = -1.258, z = -0.0805 }
}
local THREAT_AREA = {
upperLeft = { x = 1.53, z = -0.34 },
lowerRight = { x = -1.13, z = -0.92 }
}
local DECK_DISCARD_AREA = {
upperLeft = { x = -1.62, z = 0.855 },
lowerRight = { x = -2.02, z = -0.245 },
center = { x = -1.82, y = 0.5, z = 0.305 },
size = { x = 0.4, y = 3, z = 1.1 }
}
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- local positions
local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }
local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }
local DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- global position of encounter discard pile
local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }
-- used for the buttons on the right side of the playmat
-- starts off with the data for the "Upkeep" button and will then be changed
local buttonParameters = {
label = "Upkeep",
click_function = "doUpkeep",
tooltip = "Right-click to change color",
function_owner = self,
position = { x = 1.82, y = 0.1, z = -0.45 },
scale = { 0.12, 0.12, 0.12 },
width = 1000,
height = 280,
font_size = 180
}
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- translation table for slot names to characters for special font
local slotNameToChar = {
["any"] = "",
["Accessory"] = "C",
["Ally"] = "E",
["Arcane"] = "G",
["Body"] = "K",
["Hand (right)"] = "M",
["Hand (left)"] = "M",
["Hand x2"] = "N",
["Tarot"] = "A"
}
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- slot symbol for the respective slot (from top left to bottom right)
local slotData = {}
local defaultSlotData = {
-- 1st row
"any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally",
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- 2nd row
"any", "any", "any", "Accessory", "Arcane", "Arcane", "Body"
}
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
local isDrawButtonVisible = false
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- global variable to report "Dream-Enhancing Serum" status
isDES = false
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- table of type-object reference pairs of all owned objects
local ownedObjects = {}
local matColor = self.getMemo()
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
function onSave()
return JSON.encode({
playerColor = playerColor,
activeInvestigatorId = activeInvestigatorId,
isDrawButtonVisible = isDrawButtonVisible,
slotData = slotData
})
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
function onLoad(savedData)
if savedData and savedData ~= "" then
local loadedData = JSON.decode(savedData)
playerColor = loadedData.playerColor
activeInvestigatorId = loadedData.activeInvestigatorId
isDrawButtonVisible = loadedData.isDrawButtonVisible
slotData = loadedData.slotData
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
self.interactable = false
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- get object references to owned objects
ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- discard button creation
for i = 1, 6 do
makeDiscardButton(i)
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
self.createButton({
click_function = "drawEncounterCard",
function_owner = self,
position = { -1.84, 0, -0.65 },
rotation = { 0, 80, 0 },
width = 265,
height = 190
})
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
self.createButton({
click_function = "drawChaosTokenButton",
function_owner = self,
position = { 1.85, 0, -0.74 },
rotation = { 0, -45, 0 },
width = 135,
height = 135
})
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- Upkeep button: can use the default parameters for this
self.createButton(buttonParameters)
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- Slot editing button: modified default data
buttonParameters.label = "Edit Slots"
buttonParameters.click_function = "toggleSlotEditing"
buttonParameters.tooltip = "Right-click to reset slot symbols"
buttonParameters.position.z = 0.92
self.createButton(buttonParameters)
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
showDrawButton(isDrawButtonVisible)
redrawSlotSymbols()
math.randomseed(os.time())
Wait.time(function() collisionEnabled = true end, 0.1)
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- searches an area and optionally filters the result
function searchArea(origin, size, filter)
return searchLib.inArea(origin, self.getRotation(), size, filter)
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- finds all objects on the playmat and associated set aside zone.
function searchAroundSelf(filter)
local bounds = self.getBoundsNormalized()
-- Increase the width to cover the set aside zone
bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER
bounds.size.y = 1
-- Since the cast is centered on the position, shift left or right to keep the non-set aside edge
-- of the cast at the edge of the playmat
-- setAsideDirection accounts for the set aside zone being on the left or right, depending on the
-- table position of the playmat
local setAsideDirection = bounds.center.z > 0 and 1 or -1
local localCenter = self.positionToLocal(bounds.center)
localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
return searchArea(self.positionToWorld(localCenter), bounds.size, filter)
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- searches the area around the draw deck and discard pile
function searchDeckAndDiscardArea(filter)
local pos = self.positionToWorld(DECK_DISCARD_AREA.center)
local scale = self.getScale()
local size = {
x = DECK_DISCARD_AREA.size.x * scale.x,
y = DECK_DISCARD_AREA.size.y,
z = DECK_DISCARD_AREA.size.z * scale.z
}
return searchArea(pos, size, filter)
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- rounds a number to the specified amount of decimal places
---@param num number Initial value
---@param numDecimalPlaces number Amount of decimal places
---@return number: rounded number
function round(num, numDecimalPlaces)
local mult = 10 ^ (numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- edits the label of a button
---@param oldLabel string Old label of the button
---@param newLabel string New label of the button
function editButtonLabel(oldLabel, newLabel)
local buttons = self.getButtons()
for i = 1, #buttons do
if buttons[i].label == oldLabel then
self.editButton({ index = buttons[i].index, label = newLabel })
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- updates the internal "messageColor" which is used for print/broadcast statements if no player is seated
---@param clickedByColor string Colorstring of player who clicked a button
function updateMessageColor(clickedByColor)
messageColor = Player[playerColor].seated and playerColor or clickedByColor
end
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- handles discarding for a list of objects
---@param objList table List of objects to discard
function discardListOfObjects(objList)
for _, obj in ipairs(objList) do
if obj.type == "Card" or obj.type == "Deck" then
if obj.hasTag("PlayerCard") then
deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())
else
deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
-- put chaos tokens back into bag (e.g. Unrelenting)
elseif tokenChecker.isChaosToken(obj) then
chaosBagApi.returnChaosTokenToBag(obj)
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- don't touch locked objects (like the table etc.) or specific objects (like key tokens)
elseif not obj.getLock() and not obj.hasTag("DontDiscard") then
ownedObjects.Trash.putObject(obj)
2024-02-17 19:48:30 -05:00
end
end
2024-06-09 10:10:21 -04:00
end
2022-10-19 19:07:47 -04:00
2024-06-09 10:10:21 -04:00
-- build a discard button to discard from searchPosition
---@param id number Index of the discard button (from left to right, must be unique)
function makeDiscardButton(id)
local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET
local position = { xValue, 0.1, -0.94 }
local searchPosition = { -position[1], position[2], position[3] + 0.32 }
local handlerName = 'handler' .. id
self.setVar(handlerName, function()
local cardSizeSearch = { 2, 1, 3.2 }
local globalSearchPosition = self.positionToWorld(searchPosition)
local searchResult = searchArea(globalSearchPosition, cardSizeSearch)
return discardListOfObjects(searchResult)
end)
self.createButton({
label = "Discard",
click_function = handlerName,
function_owner = self,
position = position,
scale = { 0.12, 0.12, 0.12 },
width = 900,
height = 350,
font_size = 220
})
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- calls the Upkeep function with correct parameter
function doUpkeepFromHotkey(clickedByColor)
doUpkeep(_, clickedByColor)
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
function doUpkeep(_, clickedByColor, isRightClick)
if isRightClick then
changeColor(clickedByColor)
return
2024-02-17 19:48:30 -05:00
end
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
updateMessageColor(clickedByColor)
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
local forcedLearning = false
local rot = self.getRotation()
for _, obj in ipairs(searchAroundSelf()) do
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
if not (obj.getVar("do_not_ready") or false) then
local cardRotation = round(obj.getRotation().y, 0) - rot.y
local yRotDiff = 0
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
if cardRotation < 0 then
cardRotation = cardRotation + 360
end
-- rotate cards to the next multiple of 90° towards 0°
if cardRotation > 90 and cardRotation <= 180 then
yRotDiff = 90
elseif cardRotation < 270 and cardRotation > 180 then
yRotDiff = 270
end
-- set correct rotation for face-down cards
rot.z = obj.is_face_down and 180 or 0
obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:29 -05:00
2024-06-09 10:10:21 -04:00
-- detect forced learning to handle card drawing accordingly
if cardMetadata.id == "08031" then
forcedLearning = true
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- maybe replenish uses on certain cards
if cardMetadata.uses ~= nil then
tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)
end
elseif obj.type == "Deck" and forcedLearning == false then
-- check decks for forced learning
for _, deepObj in ipairs(obj.getObjects()) do
local cardMetadata = JSON.decode(deepObj.gm_notes) or {}
if cardMetadata.id == "08031" then
forcedLearning = true
end
end
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
-- flip investigator mini-card and summoned servitor mini-card
-- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)
local miniId = string.match(activeInvestigatorId, ".....") .. "-m"
for _, obj in ipairs(getObjects()) do
if obj.type == "Card" and obj.is_face_down then
local notes = JSON.decode(obj.getGMNotes())
if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
obj.flip()
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
end
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
-- gain a resource (or two if playing Jenny Barnes)
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
updateCounter({ type = "ResourceCounter", modifier = 2 })
printToColor("Gaining 2 resources (Jenny)", messageColor)
else
updateCounter({ type = "ResourceCounter", modifier = 1 })
end
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
-- draw a card (with handling for Patrice and Forced Learning)
if activeInvestigatorId == "06005" then
if forcedLearning then
printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?"
.. " Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
2024-03-10 09:56:22 -04:00
else
2024-06-09 10:10:21 -04:00
local handSize = #Player[playerColor].getHandObjects()
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
elseif activeInvestigatorId == "89001" then
printToColor("Drawing 2 cards (Subject 5U-21)", messageColor)
drawCardsWithReshuffle(2)
else
drawCardsWithReshuffle(1)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- click function for "draw 1 button" (that can be added via option panel)
function doDrawOne(_, clickedByColor)
updateMessageColor(clickedByColor)
drawCardsWithReshuffle(1)
2020-12-06 09:42:32 -05:00
end
2024-06-09 10:10:21 -04:00
-- draws the specified amount of cards (and shuffles the discard if necessary)
---@param numCards number Number of cards to draw
function drawCardsWithReshuffle(numCards)
local deckAreaObjects = getDeckAreaObjects()
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
-- Norman Withers handling
local harbinger = false
if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then
harbinger = true
elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then
local cards = deckAreaObjects.draw.getObjects()
if cards[#cards].name == "The Harbinger" then
harbinger = true
end
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:29 -05:00
2024-06-09 10:10:21 -04:00
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
return
2024-03-10 09:56:22 -04:00
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
local topCardDetected = false
if deckAreaObjects.topCard ~= nil then
deckAreaObjects.topCard.deal(1, playerColor)
topCardDetected = true
numCards = numCards - 1
if numCards == 0 then
flipTopCardFromDeck()
return
end
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
local deckSize = 1
if deckAreaObjects.draw == nil then
deckSize = 0
elseif deckAreaObjects.draw.type == "Deck" then
deckSize = #deckAreaObjects.draw.getObjects()
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
if deckSize >= numCards then
drawCards(numCards)
-- flip top card again for Norman
if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
flipTopCardFromDeck()
end
else
drawCards(deckSize)
if deckAreaObjects.discard ~= nil then
shuffleDiscardIntoDeck()
Wait.time(function()
drawCards(numCards - deckSize)
-- flip top card again for Norman
if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
flipTopCardFromDeck()
end
end, 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- get the draw deck and discard pile objects and returns the references
---@return table: string-indexed table with references to the found objects
function getDeckAreaObjects()
local deckAreaObjects = {}
for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do
if self.positionToLocal(object.getPosition()).z > 0.5 then
deckAreaObjects.discard = object
-- Norman Withers handling
elseif object.type == "Card" and not object.is_face_down then
deckAreaObjects.topCard = object
else
deckAreaObjects.draw = object
end
2023-04-22 16:56:01 -04:00
end
2024-06-09 10:10:21 -04:00
return deckAreaObjects
2023-04-22 16:56:01 -04:00
end
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
-- draws the specified number of cards (reshuffling of discard pile is handled separately)
---@param numCards number Number of cards to draw
function drawCards(numCards)
local deckAreaObjects = getDeckAreaObjects()
if deckAreaObjects.draw then
deckAreaObjects.draw.deal(numCards, playerColor)
end
end
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
function shuffleDiscardIntoDeck()
local deckAreaObjects = getDeckAreaObjects()
if not deckAreaObjects.discard.is_face_down then
deckAreaObjects.discard.flip()
end
deckAreaObjects.discard.shuffle()
deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- utility function for Norman Withers to flip the top card to the revealed side
function flipTopCardFromDeck()
Wait.time(function()
local deckAreaObjects = getDeckAreaObjects()
if deckAreaObjects.topCard then
elseif deckAreaObjects.draw then
if deckAreaObjects.draw.type == "Card" then
deckAreaObjects.draw.flip()
else
-- get bounds to know the height of the deck
local bounds = deckAreaObjects.draw.getBounds()
local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)
deckAreaObjects.draw.takeObject({ position = pos, flip = true })
end
end
end, 0.1)
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
-- discard a random non-hidden card from hand
function doDiscardOne()
local hand = Player[playerColor].getHandObjects()
if #hand == 0 then
broadcastToColor("Cannot discard from empty hand!", messageColor, "Red")
else
local choices = {}
for i = 1, #hand do
local notes = JSON.decode(hand[i].getGMNotes())
if notes ~= nil then
if notes.hidden ~= true then
table.insert(choices, i)
end
else
table.insert(choices, i)
end
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
if #choices == 0 then
broadcastToColor("Hidden cards can't be randomly discarded.", messageColor, "Orange")
return
end
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
-- get a random non-hidden card (from the "choices" table)
local num = math.random(1, #choices)
deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
local playerName = Player[playerColor].steam_name or playerColor
broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White")
end
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- slot symbol displaying
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- this will redraw the XML for the slot symbols based on the slotData table
function redrawSlotSymbols()
local xml = {}
local snapId = 0
-- use the snap point positions in the main play area for positions
for _, snap in ipairs(self.getSnapPoints()) do
if inArea(snap.position, MAIN_PLAY_AREA) then
snapId = snapId + 1
local slotName = slotData[snapId]
-- conversion from regular coordinates to XML
local x = snap.position.x * 100
local y = snap.position.z * 100
-- XML for a single slot (panel with text in the special font)
local slotXML = {
tag = "Panel",
attributes = {
id = "slotPanel" .. snapId,
scale = "0.1 0.1 1",
width = "175",
height = "175",
position = x .. " " .. y .. " -11"
},
children = {
{
tag = "Text",
attributes = {
id = "slot" .. snapId,
rotation = getSlotRotation(slotName),
fontSize = "145",
font = "font_arkhamicons",
color = "#414141CB",
text = slotNameToChar[slotName]
}
}
}
}
table.insert(xml, slotXML)
end
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
self.UI.setXmlTable(xml)
2024-01-06 21:32:07 -05:00
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
-- toggle the "slot editing mode"
function toggleSlotEditing(_, clickedByColor, isRightClick)
if isRightClick then
resetSlotSymbols()
return
2024-01-06 21:32:29 -05:00
end
2023-04-22 16:56:01 -04:00
2024-06-09 10:10:21 -04:00
updateMessageColor(clickedByColor)
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- toggle internal variable
currentlyEditingSlots = not currentlyEditingSlots
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
if currentlyEditingSlots then
editButtonLabel("Edit Slots", "Stop editing")
broadcastToColor("Click on a slot symbol (or an empty slot) to edit it.", messageColor, "Orange")
addClickFunctionToSlots()
else
editButtonLabel("Stop editing", "Edit Slots")
redrawSlotSymbols()
end
end
-- click function for slot symbols during the "slot editing mode"
function slotClickfunction(player, _, id)
local slotIndex = id:gsub("slotPanel", "")
slotIndex = tonumber(slotIndex)
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- make a list of the table keys as options for the dialog box
local slotNames = {}
for slotName, _ in pairs(slotNameToChar) do
table.insert(slotNames, slotName)
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
-- prompt player to choose symbol
player.showOptionsDialog("Choose Slot Symbol", slotNames, slotData[slotIndex],
function(chosenSlotName)
slotData[slotIndex] = chosenSlotName
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- update slot symbol
self.UI.setAttribute("slot" .. slotIndex, "text", slotNameToChar[chosenSlotName])
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- update slot rotation
self.UI.setAttribute("slot" .. slotIndex, "rotation", getSlotRotation(chosenSlotName))
end
)
2020-12-06 09:42:32 -05:00
end
2024-06-09 10:10:21 -04:00
-- helper function to rotate the left hand
function getSlotRotation(slotName)
if slotName == "Hand (left)" then
return "0 180 180"
else
return "0 0 180"
end
2020-12-06 09:42:32 -05:00
end
2024-06-09 10:10:21 -04:00
-- reset the slot symbols by making a deep copy of the default data and redrawing
function resetSlotSymbols()
slotData = {}
for _, slotName in ipairs(defaultSlotData) do
table.insert(slotData, slotName)
end
2024-01-06 21:32:29 -05:00
2024-06-09 10:10:21 -04:00
redrawSlotSymbols()
-- need to re-add the click functions if currently in edit mode
if currentlyEditingSlots then
addClickFunctionToSlots()
end
2023-01-29 19:31:52 -05:00
end
2024-06-09 10:10:21 -04:00
-- enables the click functions for editing
function addClickFunctionToSlots()
for i = 1, #slotData do
self.UI.setAttribute("slotPanel" .. i, "onClick", "slotClickfunction")
end
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
---------------------------------------------------------
2024-06-09 10:10:21 -04:00
-- color related functions
2024-03-10 09:56:22 -04:00
---------------------------------------------------------
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- changes the player color
function changeColor(clickedByColor)
local colorList = Player.getColors()
-- remove existing colors from the list of choices
for _, existingColor in ipairs(Player.getAvailableColors()) do
for i, newColor in ipairs(colorList) do
if existingColor == newColor or newColor == "Black" or newColor == "Grey" then
table.remove(colorList, i)
2023-01-29 19:31:52 -05:00
end
end
end
2024-06-09 10:10:21 -04:00
-- show the option dialog for color selection to the player that triggered this
Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color)
-- update the color of the hand zone
local handZone = ownedObjects.HandZone
handZone.setValue(color)
-- if the seated player clicked this, reseat him to the new color
if clickedByColor == playerColor then
navigationOverlayApi.copyVisibility(playerColor, color)
Player[playerColor].changeColor(color)
end
-- update the internal variable
playerColor = color
2024-03-10 09:56:22 -04:00
end)
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
---------------------------------------------------------
2024-06-09 10:10:21 -04:00
-- playmat token spawning
2024-03-10 09:56:22 -04:00
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Finds all customizable cards in this play area and updates their metadata based on the selections
-- on the matching upgrade sheet.
-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be
-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the
-- number of customizable cards in play.
function syncAllCustomizableCards()
for _, card in ipairs(searchAroundSelf("isCard")) do
syncCustomizableMetadata(card)
end
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or {}
if cardMetadata == nil or cardMetadata.customizations == nil then return end
for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do
local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}
if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then
for i, customization in ipairs(cardMetadata.customizations) do
if customization.replaces ~= nil and customization.replaces.uses ~= nil then
if upgradeSheet.call("isUpgradeActive", i) then
cardMetadata.uses = customization.replaces.uses
card.setGMNotes(JSON.encode(cardMetadata))
else
-- TODO: Get the original metadata to restore it... maybe. This should only be
-- necessary in the very unlikely case that a user un-checks a previously-full upgrade
-- row while the card is in play. It will be much easier once the AllPlayerCardsApi is
-- in place, so defer until it is
end
end
end
end
end
end
function spawnTokensFor(object)
local extraUses = {}
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
end
tokenManager.spawnForCard(object, extraUses)
end
function onCollisionEnter(collisionInfo)
local object = collisionInfo.collision_object
-- only continue if loading is completed
if not collisionEnabled then return end
-- only continue for cards
if object.type ~= "Card" then return end
-- detect if "Dream-Enhancing Serum" is placed
if object.getName() == "Dream-Enhancing Serum" then isDES = true end
maybeUpdateActiveInvestigator(object)
syncCustomizableMetadata(object)
local localCardPos = self.positionToLocal(object.getPosition())
if inArea(localCardPos, DECK_DISCARD_AREA) then
tokenManager.resetTokensSpawned(object)
removeTokensFromObject(object)
elseif shouldSpawnTokens(object) then
spawnTokensFor(object)
end
end
-- detect if "Dream-Enhancing Serum" is removed
function onCollisionExit(collisionInfo)
if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end
end
-- checks if tokens should be spawned for the provided card
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
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
-- Spawn tokens for all encounter types in the threat area
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
or metadata.type == "Enemy"
or metadata.weakness) then
return true
end
return false
end
function onObjectEnterContainer(container, object)
if object.type ~= "Card" then return end
local localCardPos = self.positionToLocal(object.getPosition())
if inArea(localCardPos, DECK_DISCARD_AREA) then
tokenManager.resetTokensSpawned(object)
removeTokensFromObject(object)
end
end
-- removes tokens from the provided card/deck
function removeTokensFromObject(object)
if object.hasTag("CardThatSeals") then
local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content)
if func ~= nil then
object.call("resetSealedTokens")
end
end
for _, obj in ipairs(searchLib.onObject(object)) do
if tokenChecker.isChaosToken(obj) then
chaosBagApi.returnChaosTokenToBag(obj)
elseif obj.getGUID() ~= "4ee1f2" and -- table
obj ~= self and
obj.type ~= "Deck" and
obj.type ~= "Card" and
obj.memo ~= nil and
obj.getLock() == false and
obj.getDescription() ~= "Action Token" then
ownedObjects.Trash.putObject(obj)
end
end
end
---------------------------------------------------------
-- investigator ID grabbing and skill tracker
---------------------------------------------------------
-- updates the internal investigator id and action tokens if an investigator card is detected
---@param card tts__Object Card that might be an investigator
function maybeUpdateActiveInvestigator(card)
if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end
local notes = JSON.decode(card.getGMNotes())
local class
if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then
if notes.id == activeInvestigatorId then return end
class = notes.class
activeInvestigatorId = notes.id
ownedObjects.InvestigatorSkillTracker.call("updateStats", {
notes.willpowerIcons,
notes.intellectIcons,
notes.combatIcons,
notes.agilityIcons
})
elseif activeInvestigatorId ~= "00000" then
class = "Neutral"
activeInvestigatorId = "00000"
ownedObjects.InvestigatorSkillTracker.call("updateStats", { 1, 1, 1, 1 })
else
2024-03-10 09:56:22 -04:00
return
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- change state of action tokens
local search = searchArea(self.positionToWorld({ -1.1, 0.05, -0.27 }), { 4, 1, 1 })
local smallToken = nil
local STATE_TABLE = {
["Guardian"] = 1,
["Seeker"] = 2,
["Rogue"] = 3,
["Mystic"] = 4,
["Survivor"] = 5,
["Neutral"] = 6
}
for _, obj in ipairs(search) do
if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
if obj.getScale().x < 0.4 then
smallToken = obj
else
setObjectState(obj, STATE_TABLE[class])
end
end
end
-- update the small token with special action for certain investigators
local SPECIAL_ACTIONS = {
["04002"] = 8, -- Ursula Downs
["01002"] = 9, -- Daisy Walker
["01502"] = 9, -- Daisy Walker
["01002-pb"] = 9, -- Daisy Walker
["06003"] = 10, -- Tony Morgan
["04003"] = 11, -- Finn Edwards
["08016"] = 14 -- Bob Jenkins
}
if smallToken ~= nil then
setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])
end
end
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
end
---------------------------------------------------------
-- manipulation of owned objects
---------------------------------------------------------
-- updates the specified owned counter
---@param param table Contains the information to update:
--- type: String Counter to target
--- newValue: Number Value to set the counter to
--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier
function updateCounter(param)
local counter = ownedObjects[param.type]
if counter ~= nil then
counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier))
else
printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow")
end
end
-- get the value the specified owned counter
---@param type string Counter to target
---@return number: Counter value
function getCounterValue(type)
return ownedObjects[type].getVar("val")
end
-- set investigator skill tracker to "1, 1, 1, 1"
function resetSkillTracker()
local obj = ownedObjects.InvestigatorSkillTracker
if obj ~= nil then
obj.call("updateStats", { 1, 1, 1, 1 })
else
printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow")
end
end
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
function drawChaosTokenButton(_, _, isRightClick)
chaosBagApi.drawChaosToken(self, isRightClick)
end
function drawEncounterCard(_, _, isRightClick)
local drawPos = getEncounterCardDrawPosition(not isRightClick)
mythosAreaApi.drawEncounterCard(matColor, drawPos)
end
function returnGlobalDiscardPosition()
return self.positionToWorld(DISCARD_PILE_POSITION)
end
function returnGlobalDrawPosition()
return self.positionToWorld(DRAW_DECK_POSITION)
end
-- returns the position for encounter card drawing
---@param stack boolean If true, returns the leftmost position instead of the first empty from the right
function getEncounterCardDrawPosition(stack)
local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)
-- maybe override position with first empty slot in threat area (right to left)
if not stack then
local searchPos = Vector(-0.91, 0.5, -0.625)
for i = 1, 5 do
local globalSearchPos = self.positionToWorld(searchPos)
local searchResult = searchLib.atPosition(globalSearchPos, "isCardOrDeck")
if #searchResult == 0 then
drawPos = globalSearchPos
break
else
searchPos.x = searchPos.x + 0.455
end
end
end
return drawPos
end
-- creates / removes the draw 1 button
---@param visible boolean Whether the draw 1 button should be visible
function showDrawButton(visible)
isDrawButtonVisible = visible
if isDrawButtonVisible then
-- Draw 1 button: modified default data
buttonParameters.label = "Draw 1"
buttonParameters.click_function = "doDrawOne"
buttonParameters.tooltip = ""
buttonParameters.position.z = -0.35
self.createButton(buttonParameters)
else
local buttons = self.getButtons()
for i = 1, #buttons do
if buttons[i].label == "Draw 1" then
self.removeButton(buttons[i].index)
end
end
end
end
-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues
---@param showCounter boolean Whether the clickable clue counter should be visible
function clickableClues(showCounter)
local clickerPos = ownedObjects.ClickableClueCounter.getPosition()
local clueCount = 0
-- move clue counters
local modY = showCounter and 0.525 or -0.525
ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))
if showCounter then
-- get current clue count
clueCount = ownedObjects.ClueCounter.getVar("exposedValue")
-- remove clues
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
-- set value for clue clickers
ownedObjects.ClickableClueCounter.call("updateVal", clueCount)
else
-- get current clue count
clueCount = ownedObjects.ClickableClueCounter.getVar("val")
-- spawn clues
local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
tokenManager.spawnToken(pos, "clue", self.getRotation())
end
end
end
-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash)
ownedObjects.ClickableClueCounter.call("updateVal", 0)
end
-- reports the clue count
---@param useClickableCounters boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
if useClickableCounters then
return ownedObjects.ClickableClueCounter.getVar("val")
else
return ownedObjects.ClueCounter.getVar("exposedValue")
end
end
-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes
-- is true, the main card slot snap points will only snap assets, while the investigator area point
-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.
---@param matchTypes boolean Whether snap points should only snap for the matching card types.
function setLimitSnapsByType(matchTypes)
local snaps = self.getSnapPoints()
for i, snap in ipairs(snaps) do
if inArea(snap.position, MAIN_PLAY_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Asset" }
else
table.insert(snaps[i].tags, "Asset")
end
else
snaps[i].tags = nil
end
end
if inArea(snap.position, INVESTIGATOR_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Investigator" }
else
table.insert(snaps[i].tags, "Investigator")
end
else
snaps[i].tags = nil
end
end
end
self.setSnapPoints(snaps)
end
-- Simple method to check if the given point is in a specified area. Local use only
---@param point tts__Vector Point to check, only x and z values are relevant
---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.
---@return boolean: True if the point is in the area defined by bounds
function inArea(point, bounds)
return (point.x < bounds.upperLeft.x
and point.x > bounds.lowerRight.x
and point.z < bounds.upperLeft.z
and point.z > bounds.lowerRight.z)
end
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
end
end)
__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local GUIDReferenceApi = {}
local function getGuidHandler()
return getObjectFromGUID("123456")
end
-- Returns the matching object
---@param owner string Parent object for this search
---@param type string Type of object to search for
---@return any: Object reference to the matching object
GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)
return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type })
end
-- Returns all matching objects as a table with references
---@param type string Type of object to search for
---@return table: List of object references to matching objects
GUIDReferenceApi.getObjectsByType = function(type)
return getGuidHandler().call("getObjectsByType", type)
end
-- Returns all matching objects as a table with references
---@param owner string Parent object for this search
---@return table: List of object references to matching objects
GUIDReferenceApi.getObjectsByOwner = function(owner)
return getGuidHandler().call("getObjectsByOwner", owner)
end
-- Sends new information to the reference handler to edit the main index
---@param owner string Parent of the object
---@param type string Type of the object
---@param guid string GUID of the object
GUIDReferenceApi.editIndex = function(owner, type, guid)
return getGuidHandler().call("editIndex", {
owner = owner,
type = type,
guid = guid
})
end
-- Returns the owner of an object or the object it's located on
---@param object tts__GameObject Object for this search
---@return string: Parent of the object or object it's located on
GUIDReferenceApi.getOwnerOfObject = function(object)
return getGuidHandler().call("getOwnerOfObject", object)
end
return GUIDReferenceApi
end
end)
__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local MythosAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
local function getMythosArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea")
end
---@return any: Table of chaos token metadata (if provided through scenario reference card)
MythosAreaApi.returnTokenData = function()
return getMythosArea().call("returnTokenData")
end
---@return any: Object reference to the encounter deck
MythosAreaApi.getEncounterDeck = function()
return getMythosArea().call("getEncounterDeck")
end
-- draw an encounter card for the requesting mat to the first empty spot from the right
---@param matColor string Playermat that triggered this
---@param position tts__Vector Position for the encounter card
MythosAreaApi.drawEncounterCard = function(matColor, position)
getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position })
end
-- reshuffle the encounter deck
MythosAreaApi.reshuffleEncounterDeck = function()
getMythosArea().call("reshuffleEncounterDeck")
end
return MythosAreaApi
end
end)
__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local CHAOS_TOKEN_NAMES = {
["Elder Sign"] = true,
["+1"] = true,
["0"] = true,
["-1"] = true,
["-2"] = true,
["-3"] = true,
["-4"] = true,
["-5"] = true,
["-6"] = true,
["-7"] = true,
["-8"] = true,
["Skull"] = true,
["Cultist"] = true,
["Tablet"] = true,
["Elder Thing"] = true,
["Auto-fail"] = true,
["Bless"] = true,
["Curse"] = true,
["Frost"] = true
}
local TokenChecker = {}
-- returns true if the passed object is a chaos token (by name)
TokenChecker.isChaosToken = function(obj)
if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then
return true
else
return false
end
end
return TokenChecker
end
end)
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local guidReferenceApi = require("core/GUIDReferenceApi")
local optionPanelApi = require("core/OptionPanelApi")
local playAreaApi = require("core/PlayAreaApi")
local searchLib = require("util/SearchLib")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
local PLAYER_CARD_TOKEN_OFFSETS = {
[1] = {
Vector(0, 3, -0.2)
},
[2] = {
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[3] = {
Vector(0, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[4] = {
Vector(0.4, 3, -0.9),
Vector(-0.4, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[5] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[6] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2)
},
[7] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0, 3, 0.5)
},
[8] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(-0.35, 3, 0.5),
Vector(0.35, 3, 0.5)
},
[9] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5)
},
[10] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0, 3, 1.2)
},
[11] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(-0.35, 3, 1.2),
Vector(0.35, 3, 1.2)
},
[12] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0.7, 3, 1.2),
Vector(0, 3, 1.2),
Vector(-0.7, 3, 1.2)
}
}
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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
}
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Table of data extracted from the token source bag, keyed by the Memo on each token which
-- should match the token type keys ("resource", "clue", etc)
local tokenTemplates
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
local playerCardData
local locationData
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
local TokenManager = {}
local internal = {}
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- Spawns tokens for the card. This function is built to just throw a card at it and let it do
-- the work once a card has hit an area where it might spawn tokens. It will check to see if
-- the card has already spawned, find appropriate data from either the uses metadata or the Data
-- Helper, and spawn the tokens.
---@param card 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 playmat should pass "Charge"=1
TokenManager.spawnForCard = function(card, extraUses)
if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then
return
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
local metadata = JSON.decode(card.getGMNotes())
if metadata ~= nil then
internal.spawnTokensFromUses(card, extraUses)
2024-03-10 09:56:22 -04:00
else
2024-06-09 10:10:21 -04:00
internal.spawnTokensFromDataHelper(card)
2024-02-17 19:48:30 -05:00
end
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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, for example "damage", "horror" or "resource"
---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the
-- 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 = optionPanelApi.getOptions()
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
if tokenType == "damage" or tokenType == "horror" then
TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
elseif tokenType == "resource" and optionPanel["useResourceCounters"] == "enabled" then
TokenManager.spawnResourceCounterToken(card, tokenCount)
elseif tokenType == "resource" and optionPanel["useResourceCounters"] == "custom" and tokenCount == 0 then
TokenManager.spawnResourceCounterToken(card, tokenCount)
else
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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, valid values are "damage" and "horror". Other
-- types should use spawnMultipleTokens()
---@param tokenValue number Value to set the damage/horror to
TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
if tokenValue < 1 or tokenValue > 50 then return end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
TokenManager.spawnToken(pos, tokenType, rot, function(spawned)
-- token starts in state 1, so don't attempt to change it to avoid error
if tokenValue ~= 1 then
spawned.setState(tokenValue)
end
end)
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
TokenManager.spawnResourceCounterToken = function(card, tokenCount)
local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
local rot = card.getRotation()
TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
spawned.call("updateVal", tokenCount)
end)
2024-01-06 21:32:07 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Spawns a number of tokens.
---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue".
-- Other types should use spawnCounterToken()
---@param tokenCount number How many tokens to spawn
---@param shiftDown? number An offset for the z-value of this group of tokens
---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource 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
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
local offsets = {}
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
2024-03-10 09:56:22 -04:00
else
2024-06-09 10:10:21 -04:00
-- only up to 12 offset tables defined
if tokenCount > 12 then return end
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
-- Fix the y-position for the spawn, since positionToWorld considers rotation which can
-- have bad results for face up/down differences
offsets[i].y = card.getPosition().y + 0.15
2024-03-10 09:56:22 -04:00
end
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
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
2024-03-10 09:56:22 -04:00
end
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
2024-03-10 09:56:22 -04:00
return
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- handling for not provided subtype (for example when spawning from custom data helpers)
if subType == nil then
subType = ""
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- this is used to load the correct state for additional resource tokens (e.g. "Ammo")
local callback = nil
local stateID = stateTable[string.lower(subType)]
if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then
callback = function(spawned) spawned.setState(stateID) end
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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, valid values are "damage", "horror",
-- "resource", "doom", or "clue"
---@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"
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
if tokenTemplates[loadTokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local tokenTemplate = tokenTemplates[loadTokenType]
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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
2024-01-06 21:32:29 -05:00
2024-06-09 10:10:21 -04:00
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
})
2024-01-06 21:32:29 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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)
---@param mat tts__Object The playmat the card is placed on (for rotation and casting)
TokenManager.maybeReplenishCard = function(card, uses, mat)
-- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)
if uses[1].count and uses[1].replenish then
internal.replenishTokens(card, uses, mat)
2024-03-10 09:56:22 -04:00
end
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some
-- callers.
---@param card tts__Object Card object to reset the tokens for
TokenManager.resetTokensSpawned = function(card)
tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- Pushes new player card data into the local copy of the Data Helper player data.
---@param dataTable table Key/Value pairs following the DataHelper style
TokenManager.addPlayerCardData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
end
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable table Key/Value pairs following the DataHelper style
TokenManager.addLocationData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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
2024-02-17 19:48:30 -05:00
end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
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
2024-02-17 19:48:30 -05:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- 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')
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card 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 playmat should pass "Charge"=1
internal.spawnTokensFromUses = function(card, extraUses)
local uses = internal.getUses(card)
if uses == nil then return end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Spawn tokens for a card based on the data helper data. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card 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)
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- 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
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
internal.getClueCountFromData = function(card, locationData)
-- Return the number of clues to spawn on this location
if locationData == nil then
error('attempted to get clue for unexpected object: ' .. card.getName())
return 0
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
if ((card.is_face_down and locationData.clueSide == 'back')
or (not card.is_face_down and locationData.clueSide == 'front')) then
if locationData.type == 'fixed' then
return locationData.value
elseif locationData.type == 'perPlayer' then
return locationData.value * playAreaApi.getInvestigatorCount()
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
error('unexpected location type: ' .. locationData.type)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
return 0
2024-02-17 19:48:30 -05:00
end
2024-01-06 21:32:07 -05:00
2024-06-09 10:10:21 -04:00
-- 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
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
return nil
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- 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)
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
table.insert(cluePositions, cluePos)
2023-01-29 19:31:52 -05:00
end
2024-06-09 10:10:21 -04:00
return cluePositions
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
---@param card tts__Object Card object to be replenished
---@param uses table The already decoded metadata.uses (to avoid decoding again)
---@param mat tts__Object The playmat the card is placed on (for rotation and casting)
internal.replenishTokens = function(card, uses, mat)
local cardPos = card.getPosition()
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- don't continue for cards on the deck (Norman) or in the discard pile
if mat.positionToLocal(cardPos).x < -1 then return end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- get current amount of resource tokens on the card
local clickableResourceCounter = nil
local foundTokens = 0
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
local memo = obj.getMemo()
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
if (stateTable[memo] or 0) > 0 then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- this is the theoretical new amount of uses (to be checked below)
local newCount = foundTokens + uses[1].replenish
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- if there are already more uses than the replenish amount, keep them
if foundTokens > uses[1].count then
newCount = foundTokens
-- only replenish up until the replenish amount
elseif newCount > uses[1].count then
newCount = uses[1].count
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)
end
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
return TokenManager
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local PlayAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
local function getPlayArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea")
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
local function getInvestigatorCounter()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter")
end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- Returns the current value of the investigator counter from the playmat
---@return number: Number of investigators currently set on the counter
PlayAreaApi.getInvestigatorCount = function()
return getInvestigatorCounter().getVar("val")
2024-01-06 21:32:07 -05:00
end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- Updates the current value of the investigator counter from the playmat
---@param count number Number of investigators to set on the counter
PlayAreaApi.setInvestigatorCount = function(count)
getInvestigatorCounter().call("updateVal", count)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'
---@param playerColor string Color of the player requesting the shift for messages
PlayAreaApi.shiftContentsUp = function(playerColor)
getPlayArea().call("shiftContentsUp", playerColor)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
PlayAreaApi.shiftContentsDown = function(playerColor)
getPlayArea().call("shiftContentsDown", playerColor)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
PlayAreaApi.shiftContentsLeft = function(playerColor)
getPlayArea().call("shiftContentsLeft", playerColor)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
PlayAreaApi.shiftContentsRight = function(playerColor)
getPlayArea().call("shiftContentsRight", playerColor)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
---@param state boolean This controls whether location connections should be drawn
PlayAreaApi.setConnectionDrawState = function(state)
getPlayArea().call("setConnectionDrawState", state)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
---@param color string Connection color to be used for location connections
PlayAreaApi.setConnectionColor = function(color)
getPlayArea().call("setConnectionColor", color)
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- Event to be called when the current scenario has changed
---@param scenarioName string Name of the new scenario
PlayAreaApi.onScenarioChanged = function(scenarioName)
getPlayArea().call("onScenarioChanged", scenarioName)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Sets this playmat's snap points to limit snapping to locations or not.
-- If matchTypes is false, snap points will be reset to snap all cards.
---@param matchCardTypes boolean Whether snap points should only snap for the matching card types
PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
getPlayArea().call("setLimitSnapsByType", matchCardTypes)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged
-- cards before they're destroyed by entering the container
PlayAreaApi.tryObjectEnterContainer = function(container, object)
getPlayArea().call("tryObjectEnterContainer", { container = container, object = object })
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Counts the VP on locations in the play area
PlayAreaApi.countVP = function()
return getPlayArea().call("countVP")
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
-- Highlights all locations in the play area without metadata
---@param state boolean True if highlighting should be enabled
PlayAreaApi.highlightMissingData = function(state)
return getPlayArea().call("highlightMissingData", state)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Highlights all locations in the play area with VP
---@param state boolean True if highlighting should be enabled
PlayAreaApi.highlightCountedVP = function(state)
return getPlayArea().call("countVP", state)
end
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
-- Checks if an object is in the play area (returns true or false)
PlayAreaApi.isInPlayArea = function(object)
return getPlayArea().call("isInPlayArea", object)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
-- Returns the current surface of the play area
PlayAreaApi.getSurface = function()
return getPlayArea().getCustomObject().image
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
-- Updates the surface of the play area
PlayAreaApi.updateSurface = function(url)
return getPlayArea().call("updateSurface", url)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
-- Returns a deep copy of the currently tracked locations
PlayAreaApi.getTrackedLocations = function()
local t = {}
for k, v in pairs(getPlayArea().call("getTrackedLocations")) do
t[k] = v
end
return t
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the
-- data to the local token manager instance.
---@param args table Single-value array holding the GUID of the Custom Data Helper making the call
PlayAreaApi.updateLocations = function(args)
getPlayArea().call("updateLocations", args)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
PlayAreaApi.getCustomDataHelper = function()
return getPlayArea().getVar("customDataHelper")
end
return PlayAreaApi
2024-02-04 10:51:51 -05:00
end
end)
2022-12-13 14:02:30 -05:00
return __bundle_require("__root")