ah_sce_unpacked/unpacked/Custom_Token Play Area 721ba2.ttslua

1560 lines
57 KiB
Plaintext
Raw Permalink 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-06-09 10:10:21 -04:00
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-04 10:51:51 -05:00
do
2024-06-09 10:10:21 -04:00
local TokenSpawnTracker = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-02-04 10:51:51 -05:00
2024-06-09 10:10:21 -04:00
local function getSpawnTracker()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker")
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getSpawnTracker().call("hasSpawnedTokens", cardGuid)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getSpawnTracker().call("markTokensSpawned", cardGuid)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getSpawnTracker().call("resetTokensSpawned", cardGuid)
2024-02-04 10:51:51 -05:00
end
2024-06-09 10:10:21 -04:00
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getSpawnTracker().call("resetAllAssetAndEvents")
2024-02-04 10:51:51 -05:00
end
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:29 -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:29 -05:00
2024-06-09 10:10:21 -04:00
return TokenSpawnTracker
2024-03-10 09:56:22 -04:00
end
end)
2024-06-09 10:10:21 -04:00
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("core/PlayArea")
end)
2024-03-10 09:56:22 -04:00
__bundle_register("core/PlayArea", function(require, _LOADED, __bundle_register, __bundle_modules)
local guidReferenceApi = require("core/GUIDReferenceApi")
local searchLib = require("util/SearchLib")
local tokenManager = require("core/token/TokenManager")
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- Location connection directional options
local BIDIRECTIONAL = 0
local ONE_WAY = 1
local INCOMING_ONE_WAY = 2
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- Connector draw parameters
local CONNECTION_THICKNESS = 0.015
local DRAGGING_CONNECTION_THICKNESS = 0.15
local DRAGGING_CONNECTION_COLOR = { 0.8, 0.8, 0.8, 1 }
local DIRECTIONAL_ARROW_DISTANCE = 3.5
local ARROW_ARM_LENGTH = 0.9
local ARROW_ANGLE = 25
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- Height to draw the connector lines, places them just above the table and always below cards
local CONNECTION_LINE_Y = 1.529
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- used for recreating the link to a custom data helper after image change
customDataHelper = nil
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
local DEFAULT_URL = "http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/"
2024-02-04 10:51:51 -05:00
2024-03-10 09:56:22 -04:00
local SHIFT_OFFSETS = {
left = { x = 0.00, y = 0, z = 7.67 },
right = { x = 0.00, y = 0, z = -7.67 },
up = { x = 6.54, y = 0, z = 0.00 },
down = { x = -6.54, y = 0, z = 0.00 }
}
local SHIFT_EXCLUSION = {
["b7b45b"] = true,
["f182ee"] = true,
["721ba2"] = true
}
local LOC_LINK_EXCLUDE_SCENARIOS = {
["The Witching Hour"] = true,
["The Heart of Madness"] = true
}
2024-02-04 10:51:51 -05:00
2024-03-10 09:56:22 -04:00
local locations = {}
local locationConnections = {}
local draggingGuids = {}
local missingData = {}
local collisionEnabled = false
local currentScenario, connectionsEnabled
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
---------------------------------------------------------
-- general code
---------------------------------------------------------
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
function onSave()
return JSON.encode({
trackedLocations = locations,
currentScenario = currentScenario,
connectionColor = connectionColor,
connectionsEnabled = connectionsEnabled
})
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function onLoad(savedData)
2024-06-09 10:10:21 -04:00
if savedData and savedData ~= "" then
local loadedData = JSON.decode(savedData) or {}
locations = loadedData.trackedLocations or {}
currentScenario = loadedData.currentScenario
connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 }
connectionsEnabled = loadedData.connectionsEnabled or true
end
-- this needs to be here since the playarea will be reloaded when the image changes
self.interactable = false
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
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
-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the
2024-03-10 09:56:22 -04:00
-- data to the local token manager instance.
---@param args table Single-value array holding the GUID of the Custom Data Helper making the call
function updateLocations(args)
customDataHelper = getObjectFromGUID(args[1])
if customDataHelper ~= nil then
tokenManager.addLocationData(customDataHelper.getTable("LOCATIONS_DATA"))
end
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- TTS event handling
---------------------------------------------------------
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function onCollisionEnter(collisionInfo)
if not collisionEnabled then return end
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
local object = collisionInfo.collision_object
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
if object.type == "Deck" then
table.insert(missingData, object)
2024-01-06 21:32:29 -05:00
end
2024-03-10 09:56:22 -04:00
-- only continue for cards
if object.type ~= "Card" then return end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
-- check if we should spawn clues here and do so according to playercount
if shouldSpawnTokens(object) then
tokenManager.spawnForCard(object)
2024-01-06 21:32:29 -05:00
end
2024-06-09 10:10:21 -04:00
-- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send
2024-03-10 09:56:22 -04:00
-- the dropped card immediately into a deck, so this has to be done here
if draggingGuids[object.getGUID()] ~= nil then
object.setVectorLines({})
draggingGuids[object.getGUID()] = nil
2024-01-06 21:32:29 -05:00
end
2024-03-10 09:56:22 -04:00
maybeTrackLocation(object)
end
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
function onCollisionExit(collisionInfo)
maybeUntrackLocation(collisionInfo.collision_object)
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well
function onObjectDestroy(object)
maybeUntrackLocation(object)
end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
function onObjectPickUp(_, object)
if object.type ~= "Card" then return end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we should be tracking
2024-03-10 09:56:22 -04:00
if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= "" then
local pickedUpGuid = object.getGUID()
local metadata = JSON.decode(object.getGMNotes()) or {}
if metadata.type == "Location" then
-- onCollisionExit sometimes comes 1 frame after onObjectPickUp (rather than before it or in
-- the same frame). This causes a mismatch in the data between dragging the on-table, and
-- that one frame draws connectors on the card which then show up as shadows for snap points.
-- Waiting ensures we always do thing in the expected Exit->PickUp order
Wait.frames(function()
if object.is_face_down then
draggingGuids[pickedUpGuid] = metadata.locationBack
else
draggingGuids[pickedUpGuid] = metadata.locationFront
end
rebuildConnectionList()
end, 2)
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end
end
2023-08-27 21:09:46 -04:00
2024-06-09 10:10:21 -04:00
-- Due to the frequence of onUpdate calls, ensure that we only process any changes once
2024-03-10 09:56:22 -04:00
function onUpdate()
local needsConnectionRebuild = false
local needsConnectionDraw = false
for guid, _ in pairs(draggingGuids) do
local obj = getObjectFromGUID(guid)
if obj == nil or not isInPlayArea(obj) then
draggingGuids[guid] = nil
needsConnectionRebuild = true
2024-06-09 10:10:21 -04:00
-- If object still exists then it's outside the area and needs to lose the lines attached to it
2024-03-10 09:56:22 -04:00
if obj ~= nil then
obj.setVectorLines({})
2024-02-17 19:48:30 -05:00
end
end
2024-03-10 09:56:22 -04:00
-- Even if the last location left the play area, need one last draw to clear the lines
needsConnectionDraw = true
end
if needsConnectionRebuild then
rebuildConnectionList()
end
if needsConnectionDraw then
drawDraggingConnections()
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end
2022-12-13 14:02:30 -05:00
2024-06-09 10:10:21 -04:00
-- Global event handler, delegated from Global. Clears any connection lines from dragged cards
-- before they are destroyed by entering a deck. Removal of the card from the dragging list will
-- be handled during the next onUpdate() call.
function tryObjectEnterContainer()
for draggedGuid, _ in pairs(draggingGuids) do
local draggedObj = getObjectFromGUID(draggedGuid)
if draggedObj ~= nil then
draggedObj.setVectorLines({})
end
end
end
---------------------------------------------------------
-- main functionality
---------------------------------------------------------
function shouldSpawnTokens(card)
local metadata = JSON.decode(card.getGMNotes())
if metadata == nil then
return tokenManager.hasLocationData(card)
end
return metadata.type == "Location"
or metadata.type == "Enemy"
or metadata.type == "Treachery"
or metadata.weakness
-- hardcoded IDs for "Makeshift Trap" and "Shrine of the Moirai"
-- these cards are events with uses, that attach to encounter cards and thus will enter play in the playarea
-- TODO: probably turn this into a metadata field if we get more cards like that
or metadata.id == "07310"
or metadata.id == "09100"
end
2024-03-10 09:56:22 -04:00
-- Checks the given card and adds it to the list of locations tracked for connection purposes.
-- A card will be added to the tracking if it is a location in the play area (based on centerpoint).
---@param card tts__Object A card object, possibly a location.
function maybeTrackLocation(card)
-- Collision checks for any part of the card overlap, but our other tracking is centerpoint
-- Ignore any collision where the centerpoint isn't in the area
if isInPlayArea(card) then
local metadata = JSON.decode(card.getGMNotes())
if metadata == nil then
table.insert(missingData, card)
else
if metadata.type == "Location" then
if card.is_face_down then
locations[card.getGUID()] = metadata.locationBack
else
locations[card.getGUID()] = metadata.locationFront
end
-- only draw connection lines for not-excluded scenarios
if showLocationLinks() then
rebuildConnectionList()
drawBaseConnections()
end
2024-02-17 19:48:30 -05:00
end
end
2024-03-10 09:56:22 -04:00
end
end
2023-01-29 19:31:52 -05:00
2024-06-09 10:10:21 -04:00
-- Stop tracking a location for connection drawing. This should be called for both collision exit
-- and destruction, as a destroyed object does not trigger collision exit. An object can also be
2024-03-10 09:56:22 -04:00
-- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will
-- be cleared in the next onUpdate() cycle.
---@param card tts__Object Card to (maybe) stop tracking
function maybeUntrackLocation(card)
-- Locked objects no longer collide (hence triggering an exit event) but are still in the play
-- area. If the object is now locked, don't remove it.
if locations[card.getGUID()] ~= nil and not card.locked then
locations[card.getGUID()] = nil
rebuildConnectionList()
drawBaseConnections()
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- Builds a list of GUID to GUID connection information based on the currently tracked locations.
-- This will update the connection information and store it in the locationConnections data member,
2024-06-09 10:10:21 -04:00
-- but does not draw those connections. This should often be followed by a call to drawBaseConnections()
2024-03-10 09:56:22 -04:00
function rebuildConnectionList()
if not showLocationLinks() then
locationConnections = {}
return
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
local iconCardList = {}
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
-- Build a list of cards with each icon as their location ID
for cardId, metadata in pairs(draggingGuids) do
buildLocListByIcon(cardId, iconCardList, metadata)
end
for cardId, metadata in pairs(locations) do
buildLocListByIcon(cardId, iconCardList, metadata)
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
-- Pair up all the icons
locationConnections = {}
for cardId, metadata in pairs(draggingGuids) do
buildConnection(cardId, iconCardList, metadata)
end
for cardId, metadata in pairs(locations) do
if draggingGuids[cardId] == nil then
buildConnection(cardId, iconCardList, metadata)
2024-02-17 19:48:30 -05:00
end
2023-01-29 19:31:52 -05:00
end
end
2024-01-06 21:32:07 -05:00
2024-03-10 09:56:22 -04:00
-- Extracts the card's icon string into a list of individual location icons
---@param cardId string GUID of the card to pull the icon data from
2024-06-09 10:10:21 -04:00
---@param iconCardList table A table of icon->GUID list. Mutable, will be updated by this method
2024-03-10 09:56:22 -04:00
---@param locData table A table containing the metadata for the card (for the correct side)
function buildLocListByIcon(cardId, iconCardList, locData)
if locData ~= nil and locData.icons ~= nil then
for icon in string.gmatch(locData.icons, "%a+") do
if iconCardList[icon] == nil then
iconCardList[icon] = {}
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
table.insert(iconCardList[icon], cardId)
2024-02-17 19:48:30 -05:00
end
2024-01-06 21:32:07 -05:00
end
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:29 -05:00
2024-03-10 09:56:22 -04:00
-- Builds the connections for the given cardID by finding matching icons and adding them to the
-- Playarea's locationConnections table.
---@param cardId string GUID of the card to build the connections for
2024-06-09 10:10:21 -04:00
---@param iconCardList table A table of icon->GUID List. Used to find matching icons for connections.
2024-03-10 09:56:22 -04:00
---@param locData table A table containing the metadata for the card (for the correct side)
function buildConnection(cardId, iconCardList, locData)
if locData ~= nil and locData.connections ~= nil then
locationConnections[cardId] = {}
for icon in string.gmatch(locData.connections, "%a+") do
if iconCardList[icon] ~= nil then
for _, connectedGuid in ipairs(iconCardList[icon]) do
-- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way
if locationConnections[connectedGuid] ~= nil
and (locationConnections[connectedGuid][cardId] == ONE_WAY
2024-06-09 10:10:21 -04:00
or locationConnections[connectedGuid][cardId] == BIDIRECTIONAL) then
2024-03-10 09:56:22 -04:00
locationConnections[connectedGuid][cardId] = BIDIRECTIONAL
locationConnections[cardId][connectedGuid] = nil
else
if locationConnections[connectedGuid] == nil then
locationConnections[connectedGuid] = {}
end
locationConnections[cardId][connectedGuid] = ONE_WAY
locationConnections[connectedGuid][cardId] = INCOMING_ONE_WAY
end
end
end
end
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end
2024-01-06 21:32:07 -05:00
2024-03-10 09:56:22 -04:00
-- Draws the lines for connections currently in locationConnections but not in draggingGuids.
function drawBaseConnections()
if not showLocationLinks() then
locationConnections = {}
self.setVectorLines({})
return
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
local cardConnectionLines = {}
2024-01-06 21:32:07 -05:00
2024-03-10 09:56:22 -04:00
for originGuid, targetGuids in pairs(locationConnections) do
-- Objects should reliably exist at this point, but since this can be called during onUpdate the
-- object checks are conservative just to make sure.
local origin = getObjectFromGUID(originGuid)
if draggingGuids[originGuid] == nil and origin ~= nil then
for targetGuid, direction in pairs(targetGuids) do
local target = getObjectFromGUID(targetGuid)
if draggingGuids[targetGuid] == nil and target ~= nil then
-- Since we process the full list, we're guaranteed to hit any ONE_WAY connections later
-- so we can ignore INCOMING_ONE_WAY
if direction == BIDIRECTIONAL then
addBidirectionalVector(origin, target, self, cardConnectionLines)
elseif direction == ONE_WAY then
addOneWayVector(origin, target, self, cardConnectionLines)
end
end
end
end
2024-01-06 21:32:07 -05:00
end
2024-03-10 09:56:22 -04:00
self.setVectorLines(cardConnectionLines)
2024-01-06 21:32:07 -05:00
end
2024-03-10 09:56:22 -04:00
-- Draws the lines for cards which are currently being dragged.
function drawDraggingConnections()
2024-06-09 10:10:21 -04:00
if not showLocationLinks() then return end
2024-03-10 09:56:22 -04:00
local ownedVectors = {}
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
for originGuid, _ in pairs(draggingGuids) do
targetGuids = locationConnections[originGuid]
-- Objects should reliably exist at this point, but since this can be called during onUpdate the
-- object checks are conservative just to make sure.
local origin = getObjectFromGUID(originGuid)
if draggingGuids[originGuid] and origin ~= nil and targetGuids ~= nil then
ownedVectors[originGuid] = {}
for targetGuid, direction in pairs(targetGuids) do
local target = getObjectFromGUID(targetGuid)
if target ~= nil then
if direction == BIDIRECTIONAL then
addBidirectionalVector(origin, target, origin, ownedVectors[originGuid])
elseif direction == ONE_WAY then
addOneWayVector(origin, target, origin, ownedVectors[originGuid])
elseif direction == INCOMING_ONE_WAY and not draggingGuids[targetGuid] then
addOneWayVector(target, origin, origin, ownedVectors[originGuid])
end
end
end
end
end
for ownerGuid, vectors in pairs(ownedVectors) do
local card = getObjectFromGUID(ownerGuid)
card.setVectorLines(vectors)
2023-04-22 16:56:01 -04:00
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
-- Draws a bidirectional location connection between the two cards, adding the necessary lines to the list
2024-03-10 09:56:22 -04:00
---@param card1 tts__Object One of the card objects to connect
---@param card2 tts__Object The other card object to connect
---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative
--- positioning and scaling, as well as highlighting connections during a drag operation
2024-06-09 10:10:21 -04:00
---@param lines table List of vector line elements. Mutable, will be updated to add this connector
2024-03-10 09:56:22 -04:00
function addBidirectionalVector(card1, card2, vectorOwner, lines)
local cardPos1 = card1.getPosition()
local cardPos2 = card2.getPosition()
cardPos1.y = CONNECTION_LINE_Y
cardPos2.y = CONNECTION_LINE_Y
local pos1 = vectorOwner.positionToLocal(cardPos1)
local pos2 = vectorOwner.positionToLocal(cardPos2)
table.insert(lines, {
points = { pos1, pos2 },
color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,
thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,
})
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
-- Draws a one-way location connection between the two cards, adding the lines to do so to the
2024-06-09 10:10:21 -04:00
-- given lines list. Arrows will point towards the target card.
2024-03-10 09:56:22 -04:00
---@param origin tts__Object Origin card in the connection
---@param target tts__Object Target card object to connect
---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative
--- positioning and scaling, as well as highlighting connections during a drag operation
---@param lines table List of vector line elements. Mutable, will be updated to add this connector
function addOneWayVector(origin, target, vectorOwner, lines)
-- Start with the BiDi then add the arrow lines to it
addBidirectionalVector(origin, target, vectorOwner, lines)
local originPos = origin.getPosition()
local targetPos = target.getPosition()
originPos.y = CONNECTION_LINE_Y
targetPos.y = CONNECTION_LINE_Y
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- Calculate distance to be closer for horizontal positions than vertical, since cards are taller than wide
2024-03-10 09:56:22 -04:00
local heading = Vector(originPos):sub(targetPos):heading("y")
local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading)))
2020-12-06 09:42:32 -05:00
2024-06-09 10:10:21 -04:00
-- Calculate the three possible arrow positions. These are offset by half the arrow length to
2024-03-10 09:56:22 -04:00
-- make them visually balanced by keeping the arrows centered, not tracking the point
local midpoint = Vector(originPos):add(targetPos):scale(0.5):moveTowards(targetPos, ARROW_ARM_LENGTH / 2)
local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2)
local closeToTarget = Vector(targetPos):moveTowards(originPos, distanceFromCard - ARROW_ARM_LENGTH / 2)
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
if (originPos:distance(closeToOrigin) > originPos:distance(closeToTarget)) then
addArrowLines(midpoint, originPos, vectorOwner, lines)
else
addArrowLines(closeToOrigin, originPos, vectorOwner, lines)
addArrowLines(closeToTarget, originPos, vectorOwner, lines)
2024-02-17 19:48:30 -05:00
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
-- Draws an arrowhead at the given position.
---@param arrowheadPos tts__Vector Centerpoint of the arrowhead to draw (NOT the tip of the arrow)
---@param originPos tts__Vector Origin point of the connection, used to position the arrow arms
---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative
--- positioning and scaling, as well as highlighting connections during a drag operation
---@param lines table List of vector line elements. Mutable, will be updated to add this arrow
function addArrowLines(arrowheadPos, originPos, vectorOwner, lines)
local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", -1 * ARROW_ANGLE):add(arrowheadPos)
local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", ARROW_ANGLE):add(arrowheadPos)
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
local head = vectorOwner.positionToLocal(arrowheadPos)
local arm1 = vectorOwner.positionToLocal(arrowArm1)
local arm2 = vectorOwner.positionToLocal(arrowArm2)
table.insert(lines, {
points = { arm1, head, arm2 },
color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,
thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,
})
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- count victory points on 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
function countVP(highlightOff)
local totalVP = 0
for cardId, metadata in pairs(locations) do
local card = getObjectFromGUID(cardId)
if metadata ~= nil and card ~= nil then
if highlightOff == true then
card.highlightOff("Green")
end
local cardVP = tonumber(metadata.victory) or 0
if cardVP ~= 0 and not cardHasClues(card) then
totalVP = totalVP + cardVP
if highlightOff == false then
card.highlightOn("Green")
end
end
end
end
return totalVP
end
-- checks if a card has clues on it, returns true if clues are on it
---@param card tts__Object Card to check for clues
function cardHasClues(card)
local searchResult = searchLib.onObject(card, "isClue")
return #searchResult > 0
end
-- highlights all locations in the play area without metadata
---@param state boolean True if highlighting should be enabled
function highlightMissingData(state)
for i, obj in pairs(missingData) do
if obj ~= nil then
if state then
obj.highlightOff("Red")
else
obj.highlightOn("Red")
end
else
missingData[i] = nil
end
end
end
---------------------------------------------------------
-- functions for outside calls
---------------------------------------------------------
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
2024-03-10 09:56:22 -04:00
-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'
2024-06-09 10:10:21 -04:00
---@param playerColor string Color of the player requesting the shift (used for error messages)
2024-03-10 09:56:22 -04:00
function shiftContentsUp(playerColor)
shiftContents(playerColor, "up")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function shiftContentsDown(playerColor)
shiftContents(playerColor, "down")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function shiftContentsLeft(playerColor)
shiftContents(playerColor, "left")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function shiftContentsRight(playerColor)
shiftContents(playerColor, "right")
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
function shiftContents(playerColor, direction)
local zone = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayAreaZone")
if not zone then
broadcastToColor("Scripting zone couldn't be found.", playerColor, "Red")
return
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
for _, object in ipairs(zone.getObjects()) do
if not (SHIFT_EXCLUSION[object.getGUID()] or object.hasTag("displacement_excluded")) then
object.translate(SHIFT_OFFSETS[direction])
end
end
Wait.time(drawBaseConnections, 0.1)
end
2024-06-09 10:10:21 -04:00
-- sets the image of the playarea
---@param newURL string URL for the new surface image
function updateSurface(newURL)
local customInfo = self.getCustomObject()
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
if newURL ~= "" and newURL ~= nil and newURL ~= DEFAULT_URL then
customInfo.image = newURL
broadcastToAll("New Playarea Image Applied", "Green")
else
customInfo.image = DEFAULT_URL
broadcastToAll("Default Playarea Image Applied", "Green")
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
self.setCustomObject(customInfo)
local guid = nil
if customDataHelper then
guid = customDataHelper.getGUID()
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
self.reload()
if guid ~= nil then
Wait.time(function() updateLocations({ guid }) end, 1)
end
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
-- Toggles the tags for the playarea's snap points to limit snapping to locations or not
-- 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
2024-03-10 09:56:22 -04:00
function setLimitSnapsByType(matchTypes)
local snaps = self.getSnapPoints()
2024-06-09 10:10:21 -04:00
for _, snap in ipairs(snaps) do
2024-03-10 09:56:22 -04:00
if matchTypes then
2024-06-09 10:10:21 -04:00
if snap.tags == nil then
snap.tags = { "Location" }
2024-03-10 09:56:22 -04:00
else
2024-06-09 10:10:21 -04:00
table.insert(snap.tags, "Location")
2024-03-10 09:56:22 -04:00
end
else
2024-06-09 10:10:21 -04:00
snap.tags = nil
2024-03-10 09:56:22 -04:00
end
end
self.setSnapPoints(snaps)
end
-- called by the option panel to enabled / disable location connections
function setConnectionDrawState(state)
connectionsEnabled = state
rebuildConnectionList()
drawBaseConnections()
end
-- called by the option panel to edit the location connection color
function setConnectionColor(color)
connectionColor = color
rebuildConnectionList()
drawBaseConnections()
end
2024-06-09 10:10:21 -04:00
function onScenarioChanged(scenarioName)
currentScenario = scenarioName
if not showLocationLinks() then
broadcastToAll("Automatic location connections not available for this scenario")
2024-03-10 09:56:22 -04:00
end
end
2024-06-09 10:10:21 -04:00
function getTrackedLocations()
return locations
2024-03-10 09:56:22 -04:00
end
2024-06-09 10:10:21 -04:00
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- Check to see if the given object is within the bounds of the play area (using X and Z coordinates)
---@param object tts__Object Object to check
---@return boolean: True if the object is inside the play area
function isInPlayArea(object)
local bounds = self.getBounds()
local position = object.getPosition()
-- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up
local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2 }
local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2 }
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z
end
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
function showLocationLinks()
return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario] and connectionsEnabled
2024-03-10 09:56:22 -04:00
end
function round(num, numDecimalPlaces)
local mult = 10 ^ (numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
2024-06-09 10:10:21 -04:00
-- rebuilds local snap points (could be useful in the future again)
function buildSnaps()
local upperleft = { x = 1.53, z = -1.09 }
local lowerright = { x = -1.53, z = 1.55 }
local snaps = {}
2024-03-10 09:56:22 -04:00
2024-06-09 10:10:21 -04:00
-- creates 81 snap points, for uneven rows + columns it makes a rotation snap point
for i = 1, 9 do
for j = 1, 9 do
local snap = {}
snap.position = {}
snap.position.x = round(upperleft.x - (upperleft.x - lowerright.x) * (i - 1) / 8, 3)
snap.position.y = 0.1
snap.position.z = round(upperleft.z - (upperleft.z - lowerright.z) * (j - 1) / 8, 3)
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
-- enable rotation snaps for uneven rows / columns
if (i % 2 ~= 0) and (j % 2 ~= 0) then
snap.rotation = { 0, 0, 0 }
snap.rotation_snap = true
end
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
table.insert(snaps, snap)
end
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
self.setSnapPoints(snaps)
end
end)
__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local GUIDReferenceApi = {}
2024-02-17 19:48:30 -05:00
2024-06-09 10:10:21 -04:00
local function getGuidHandler()
return getObjectFromGUID("123456")
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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 })
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- Sends new information to the reference handler to edit the main index
---@param owner string Parent of the object
---@param type string Type of the object
---@param guid string GUID of the object
GUIDReferenceApi.editIndex = function(owner, type, guid)
return getGuidHandler().call("editIndex", {
owner = owner,
type = type,
guid = guid
})
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
-- 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)
2024-02-17 19:48:30 -05:00
end
2024-06-09 10:10:21 -04:00
return GUIDReferenceApi
2024-02-17 19:48:30 -05:00
end
end)
2024-03-10 09:56:22 -04:00
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
2024-02-17 19:48:30 -05:00
do
local guidReferenceApi = require("core/GUIDReferenceApi")
2024-03-10 09:56:22 -04:00
local optionPanelApi = require("core/OptionPanelApi")
local playAreaApi = require("core/PlayAreaApi")
local searchLib = require("util/SearchLib")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -04:00
local PLAYER_CARD_TOKEN_OFFSETS = {
[1] = {
Vector(0, 3, -0.2)
},
[2] = {
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[3] = {
Vector(0, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[4] = {
Vector(0.4, 3, -0.9),
Vector(-0.4, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[5] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[6] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2)
},
[7] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0, 3, 0.5)
},
[8] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(-0.35, 3, 0.5),
Vector(0.35, 3, 0.5)
},
[9] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5)
},
[10] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0, 3, 1.2)
},
[11] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(-0.35, 3, 1.2),
Vector(0.35, 3, 1.2)
},
[12] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0.7, 3, 1.2),
Vector(0, 3, 1.2),
Vector(-0.7, 3, 1.2)
}
}
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -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-03-10 09:56:22 -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
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -04:00
local playerCardData
local locationData
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
local TokenManager = {}
local internal = {}
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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
end
local metadata = JSON.decode(card.getGMNotes())
if metadata ~= nil then
internal.spawnTokensFromUses(card, extraUses)
else
internal.spawnTokensFromDataHelper(card)
2020-12-06 09:42:32 -05:00
end
end
2024-03-10 09:56:22 -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()
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)
2023-01-29 19:31:52 -05:00
end
end
2024-03-10 09:56:22 -04:00
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.
2024-03-10 09:56:22 -04:00
---@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
local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
2024-06-09 10:10:21 -04:00
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)
2020-12-06 09:42:32 -05:00
end
2024-03-10 09:56:22 -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)
2023-01-29 19:31:52 -05:00
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -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-04-22 16:56:01 -04:00
2024-03-10 09:56:22 -04:00
local offsets = {}
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
else
-- only up to 12 offset tables defined
if tokenCount > 12 then return end
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
-- Fix the y-position for the spawn, since positionToWorld considers rotation which can
-- have bad results for face up/down differences
offsets[i].y = card.getPosition().y + 0.15
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -04:00
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = {}
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -04:00
-- get a vector for the shifting (downwards local to the card)
local shiftDownVector = Vector(0, 0, shiftDown):rotateOver("y", card.getRotation().y)
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset + shiftDownVector
end
2023-01-29 19:31:52 -05:00
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
-- handling for not provided subtype (for example when spawning from custom data helpers)
if subType == nil then
subType = ""
end
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)
2023-01-29 19:31:52 -05:00
end
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -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"
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -04:00
if tokenTemplates[loadTokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local tokenTemplate = tokenTemplates[loadTokenType]
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
end
2020-12-06 09:42:32 -05:00
2024-03-10 09:56:22 -04:00
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
})
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -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)
2023-01-29 19:31:52 -05:00
end
end
2024-03-10 09:56:22 -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())
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
end
end
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -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
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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')
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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-03-10 09:56:22 -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)
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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)
end
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
2024-03-10 09:56:22 -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-03-10 09:56:22 -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-03-10 09:56:22 -04:00
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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()
end
error('unexpected location type: ' .. locationData.type)
end
return 0
end
-- Gets the right uses structure for this card, based on metadata and face up/down state
---@param card tts__Object Card to pull the uses from
internal.getUses = function(card)
local metadata = JSON.decode(card.getGMNotes()) or {}
if metadata.type == "Location" then
if card.is_face_down and metadata.locationBack ~= nil then
return metadata.locationBack.uses
elseif not card.is_face_down and metadata.locationFront ~= nil then
return metadata.locationFront.uses
end
elseif not card.is_face_down then
return metadata.uses
end
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -04:00
return nil
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -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-03-10 09:56:22 -04:00
return cluePositions
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -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-03-10 09:56:22 -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
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -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-03-10 09:56:22 -04:00
for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
local memo = obj.getMemo()
2023-01-29 19:31:52 -05:00
2024-03-10 09:56:22 -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
2023-01-29 19:31:52 -05:00
end
2024-03-10 09:56:22 -04:00
end
-- this is the theoretical new amount of uses (to be checked below)
local newCount = foundTokens + uses[1].replenish
-- if there are already more uses than the replenish amount, keep them
if foundTokens > uses[1].count then
newCount = foundTokens
-- only replenish up until the replenish amount
elseif newCount > uses[1].count then
newCount = uses[1].count
end
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
2024-02-17 19:48:30 -05:00
else
2024-03-10 09:56:22 -04:00
TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)
2023-01-29 19:31:52 -05:00
end
end
2024-03-10 09:56:22 -04:00
return TokenManager
2024-02-17 19:48:30 -05:00
end
2024-03-10 09:56:22 -04:00
end)
__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local SearchLib = {}
local filterFunctions = {
isActionToken = function(x) return x.getDescription() == "Action Token" end,
isCard = function(x) return x.type == "Card" end,
isDeck = function(x) return x.type == "Deck" end,
isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end,
isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end,
isTileOrToken = function(x) return x.type == "Tile" end
}
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -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
})
-- 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)
2023-08-27 21:09:46 -04:00
end
end
2024-03-10 09:56:22 -04:00
return objList
2024-02-17 19:48:30 -05:00
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -04:00
-- searches the specified area
SearchLib.inArea = function(pos, rot, size, filter)
return returnSearchResult(pos, rot, size, filter)
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -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)
end
2023-08-27 21:09:46 -04:00
2024-03-10 09:56:22 -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)
end
2024-02-17 19:48:30 -05:00
2024-03-10 09:56:22 -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)
2023-08-27 21:09:46 -04:00
end
2024-03-10 09:56:22 -04:00
return SearchLib
2023-01-29 19:31:52 -05:00
end
end)
2024-06-09 10:10:21 -04:00
__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local OptionPanelApi = {}
-- loads saved options
---@param options table Set a new state for the option table
OptionPanelApi.loadSettings = function(options)
return Global.call("loadSettings", options)
end
---@return any: Table of option panel state
OptionPanelApi.getOptions = function()
return Global.getTable("optionPanel")
end
return OptionPanelApi
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local PlayAreaApi = {}
local guidReferenceApi = require("core/GUIDReferenceApi")
local function getPlayArea()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea")
end
local function getInvestigatorCounter()
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter")
end
-- 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")
end
-- 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
-- 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)
end
PlayAreaApi.shiftContentsDown = function(playerColor)
getPlayArea().call("shiftContentsDown", playerColor)
end
PlayAreaApi.shiftContentsLeft = function(playerColor)
getPlayArea().call("shiftContentsLeft", playerColor)
end
PlayAreaApi.shiftContentsRight = function(playerColor)
getPlayArea().call("shiftContentsRight", playerColor)
end
---@param state boolean This controls whether location connections should be drawn
PlayAreaApi.setConnectionDrawState = function(state)
getPlayArea().call("setConnectionDrawState", state)
end
---@param color string Connection color to be used for location connections
PlayAreaApi.setConnectionColor = function(color)
getPlayArea().call("setConnectionColor", color)
end
-- 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
-- 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
-- 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
-- Counts the VP on locations in the play area
PlayAreaApi.countVP = function()
return getPlayArea().call("countVP")
end
-- 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
-- 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
-- Checks if an object is in the play area (returns true or false)
PlayAreaApi.isInPlayArea = function(object)
return getPlayArea().call("isInPlayArea", object)
end
-- Returns the current surface of the play area
PlayAreaApi.getSurface = function()
return getPlayArea().getCustomObject().image
end
-- Updates the surface of the play area
PlayAreaApi.updateSurface = function(url)
return getPlayArea().call("updateSurface", url)
end
-- 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
end
-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the
-- data to the local token manager instance.
---@param args table Single-value array holding the GUID of the Custom Data Helper making the call
PlayAreaApi.updateLocations = function(args)
getPlayArea().call("updateLocations", args)
end
PlayAreaApi.getCustomDataHelper = function()
return getPlayArea().getVar("customDataHelper")
end
return PlayAreaApi
end
end)
2022-12-13 14:02:30 -05:00
return __bundle_require("__root")