ah_sce_unpacked/unpacked/Custom_Token Playarea 721ba...

1199 lines
44 KiB
Plaintext

-- 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)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("core/PlayArea")
end)
__bundle_register("core/PlayArea", function(require, _LOADED, __bundle_register, __bundle_modules)
---------------------------------------------------------
-- general setup
---------------------------------------------------------
-- set true to enable debug logging
local DEBUG = false
-- Location connection directional options
local BIDIRECTIONAL = 0
local ONE_WAY = 1
local INCOMING_ONE_WAY = 2
-- 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 CONNECTION_COLOR = { 0.4, 0.4, 0.4, 1 }
local DIRECTIONAL_ARROW_DISTANCE = 3.5
local ARROW_ARM_LENGTH = 0.9
local ARROW_ANGLE = 25
-- Height to draw the connector lines, places them just above the table and always below cards
local CONNECTION_LINE_Y = 1.529
-- we use this to turn off collision handling until onLoad() is complete
local collisionEnabled = false
-- used for recreating the link to a custom data helper after image change
customDataHelper = nil
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,
}
local tokenManager = require("core/token/TokenManager")
local INVESTIGATOR_COUNTER_GUID = "f182ee"
local PLAY_AREA_ZONE_GUID = "a2f932"
local clueData = {}
local spawnedLocationGUIDs = {}
local locations = {}
local locationConnections = {}
local draggingGuids = {}
local locationData
local currentScenario
---------------------------------------------------------
-- general code
---------------------------------------------------------
function onSave()
return JSON.encode({
trackedLocations = locations,
currentScenario = currentScenario,
})
end
function onLoad(saveState)
-- records locations we have spawned clues for
local save = JSON.decode(saveState) or { }
locations = save.trackedLocations or { }
currentScenario = save.currentScenario
self.interactable = DEBUG
Wait.time(function() collisionEnabled = true end, 1)
end
function log(message)
if DEBUG then print(message) end
end
-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the
-- data to the local token manager instance.
---@param args Table Single-value array holding the GUID of the Custom Data Helper making the call
function updateLocations(args)
customDataHelper = getObjectFromGUID(args[1])
if customDataHelper ~= nil then
tokenManager.addLocationData(customDataHelper.getTable("LOCATIONS_DATA"))
end
end
function onCollisionEnter(collisionInfo)
local obj = collisionInfo.collision_object
local objType = obj.name
-- only continue for cards
if not collisionEnabled or (objType ~= "Card" and objType ~= "CardCustom") then return end
-- check if we should spawn clues here and do so according to playercount
local card = collisionInfo.collision_object
if shouldSpawnTokens(card) then
tokenManager.spawnForCard(card)
end
-- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send
-- the dropped card immediately into a deck, so this has to be done here
if draggingGuids[card.getGUID()] ~= nil then
card.setVectorLines(nil)
draggingGuids[card.getGUID()] = nil
end
maybeTrackLocation(card)
end
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
end
function onCollisionExit(collisionInfo)
maybeUntrackLocation(collisionInfo.collision_object)
end
-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well
function onObjectDestroy(object)
maybeUntrackLocation(object)
end
function onObjectPickUp(player, object)
-- only continue for cards
local objType = object.name
if objType ~= "Card" and objType ~= "CardCustom" then return end
-- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we
-- should be tracking
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()
draggingGuids[pickedUpGuid] = metadata
rebuildConnectionList()
end, 2)
end
end
end
function onUpdate()
-- Due to the frequence of onUpdate calls, ensure that we only process any changes to the
-- connection list once, and only redraw once
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
-- If object still exists then it's been dragged outside the area and needs to clear the
-- lines attached to it
if obj ~= nil then
obj.setVectorLines(nil)
end
end
-- 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()
end
end
-- 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 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 showLocationLinks() and isInPlayArea(card) then
local metadata = JSON.decode(card.getGMNotes()) or { }
if metadata.type == "Location" then
locations[card.getGUID()] = metadata
rebuildConnectionList()
drawBaseConnections()
end
end
end
-- 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
-- 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 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()
end
end
-- 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(params)
for draggedGuid, _ in pairs(draggingGuids) do
local draggedObj = getObjectFromGUID(draggedGuid)
if draggedObj ~= nil then
draggedObj.setVectorLines(nil)
end
end
end
-- 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,
-- but does not draw those connections. This should often be followed by a call to
-- drawBaseConnections()
function rebuildConnectionList()
if not showLocationLinks() then
locationConnections = { }
return
end
local iconCardList = { }
-- Build a list of cards with each icon as their location ID
for cardId, metadata in pairs(draggingGuids) do
buildLocListByIcon(cardId, iconCardList)
end
for cardId, metadata in pairs(locations) do
buildLocListByIcon(cardId, iconCardList)
end
-- Pair up all the icons
locationConnections = { }
for cardId, metadata in pairs(draggingGuids) do
buildConnection(cardId, iconCardList)
end
for cardId, metadata in pairs(locations) do
if draggingGuids[cardId] == nil then
buildConnection(cardId, iconCardList)
end
end
end
-- 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
---@param iconCardList Table A table of icon->GUID list. Mutable, will be updated by this method
function buildLocListByIcon(cardId, iconCardList)
local card = getObjectFromGUID(cardId)
local locData = getLocationData(card)
if locData ~= nil and locData.icons ~= nil then
for icon in string.gmatch(locData.icons, "%a+") do
if iconCardList[icon] == nil then
iconCardList[icon] = { }
end
table.insert(iconCardList[icon], card.getGUID())
end
end
end
-- 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
---@param iconCardList Table A table of icon->GUID List. Used to find matching icons for connections.
function buildConnection(cardId, iconCardList)
local card = getObjectFromGUID(cardId)
local locData = getLocationData(card)
if locData ~= nil and locData.connections ~= nil then
locationConnections[card.getGUID()] = { }
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][card.getGUID()] == ONE_WAY then
locationConnections[connectedGuid][card.getGUID()] = BIDIRECTIONAL
locationConnections[card.getGUID()][connectedGuid] = nil
else
if locationConnections[connectedGuid] == nil then
locationConnections[connectedGuid] = { }
end
locationConnections[card.getGUID()][connectedGuid] = ONE_WAY
locationConnections[connectedGuid][card.getGUID()] = INCOMING_ONE_WAY
end
end
end
end
end
end
-- Helper method to extract the location metadata from a card based on whether it's front or back
-- is showing.
---@param card String Card object to extract data from
---@return. Table with either the locationFront or locationBack metadata structure, or nil if the
-- metadata doesn't exist
function getLocationData(card)
if card == nil then
return nil
end
if card.is_face_down then
return JSON.decode(card.getGMNotes()).locationBack
else
return JSON.decode(card.getGMNotes()).locationFront
end
end
-- Draws the lines for connections currently in locationConnections but not in draggingGuids.
-- Constructed vectors will be set to the playmat
function drawBaseConnections()
if not showLocationLinks() then
locationConnections = { }
return
end
local cardConnectionLines = { }
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
end
self.setVectorLines(cardConnectionLines)
end
-- Draws the lines for cards which are currently being dragged.
function drawDraggingConnections()
if not showLocationLinks() then
return
end
local cardConnectionLines = { }
local ownedVectors = { }
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)
end
end
-- Draws a bidirectional location connection between the two cards, adding the lines to do so to the
-- given lines list.
---@param card1 Object One of the card objects to connect
---@param card2 Object The other card object to connect
---@param vectorOwner 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 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 CONNECTION_COLOR or DRAGGING_CONNECTION_COLOR,
thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,
})
end
-- Draws a one-way location connection between the two cards, adding the lines to do so to the
-- given lines list. Arrows will point towards the target card.
---@param origin Object Origin card in the connection
---@param target Object Target card object to connect
---@param vectorOwner 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
-- Calculate card distance to be closer for horizontal positions than vertical, since cards are
-- taller than they are wide
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)))
-- Calculate the three possible arrow positions. These are offset by half the arrow length to
-- make them visually balanced by keeping the arrows centered, not tracking the point
local midpoint = Vector(originPos):add(targetPos):scale(Vector(0.5, 0.5, 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)
if (originPos:distance(closeToOrigin) > originPos:distance(closeToTarget)) then
addArrowLines(midpoint, originPos, vectorOwner, lines)
else
addArrowLines(closeToOrigin, originPos, vectorOwner, lines)
addArrowLines(closeToTarget, originPos, vectorOwner, lines)
end
end
-- Draws an arrowhead at the given position.
---@param arrowheadPosition Table Centerpoint of the arrowhead to draw (NOT the tip of the arrow)
---@param originPos Table Origin point of the connection, used to position the arrow arms
---@param vectorOwner 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)
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 CONNECTION_COLOR or DRAGGING_CONNECTION_COLOR,
thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,
})
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. Used solely to send an error
--- message in the unlikely case that the scripting zone has been deleted
function shiftContentsUp(playerColor)
shiftContents(playerColor, "up")
end
function shiftContentsDown(playerColor)
shiftContents(playerColor, "down")
end
function shiftContentsLeft(playerColor)
shiftContents(playerColor, "left")
end
function shiftContentsRight(playerColor)
shiftContents(playerColor, "right")
end
function shiftContents(playerColor, direction)
local zone = getObjectFromGUID(PLAY_AREA_ZONE_GUID)
if not zone then
broadcastToColor("Scripting zone couldn't be found.", playerColor, "Red")
return
end
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
end
-- Returns the current value of the investigator counter from the playmat
---@return. Number of investigators currently set on the counter
function getInvestigatorCount()
local investigatorCounter = getObjectFromGUID("f182ee")
return investigatorCounter.getVar("val")
end
-- Check to see if the given object is within the bounds of the play area, based solely on the X and
-- Z coordinates, ignoring height
---@param object Object Object to check
---@return. 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}
return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z
end
-- Reset the play area's tracking of which cards have had tokens spawned.
function resetSpawnedCards()
spawnedLocationGUIDs = {}
end
function onScenarioChanged(scenarioName)
currentScenario = scenarioName
if not showLocationLinks() then
broadcastToAll("Automatic location connections not available for this scenario")
end
end
function showLocationLinks()
return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario]
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 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
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Location" }
else
table.insert(snaps[i].tags, "Location")
end
else
snaps[i].tags = nil
end
end
self.setSnapPoints(snaps)
end
-- 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 = {}
-- 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)
-- 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
table.insert(snaps, snap)
end
end
self.setSnapPoints(snaps)
end
-- utility function
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
end)
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local tokenSpawnTracker = require("core/token/TokenSpawnTrackerApi")
local playArea = require("core/PlayAreaApi")
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)
}
}
-- Source for tokens
local TOKEN_SOURCE_GUID = "124381"
-- Table of data extracted from the token source bag, keyed by the Memo on each token which
-- should match the token type keys ("resource", "clue", etc)
local tokenTemplates
local DATA_HELPER_GUID = "708279"
local playerCardData
local locationData
local TokenManager = { }
local internal = { }
-- Spawns tokens for the card. This function is built to just throw a card at it and let it do
-- the work 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 Object Card to maybe spawn tokens for
---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
--- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1
TokenManager.spawnForCard = function(card, extraUses)
if tokenSpawnTracker.hasSpawnedTokens(card.getGUID()) then
return
end
local metadata = JSON.decode(card.getGMNotes())
if metadata ~= nil then
internal.spawnTokensFromUses(card, extraUses)
else
internal.spawnTokensFromDataHelper(card)
end
end
-- Spawns a set of tokens on the given card.
---@param card Object Card to spawn tokens on
---@param tokenType String type of token to spawn, valid values are "damage", "horror",
-- "resource", "doom", or "clue"
---@param tokenCount 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
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown)
local optionPanel = Global.getTable("optionPanel")
if tokenType == "damage" or tokenType == "horror" then
TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
elseif tokenType == "resource" and optionPanel["useResourceCounters"] then
TokenManager.spawnResourceCounterToken(card, tokenCount)
else
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown)
end
end
-- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror
-- tokens.
---@param card Object Card to spawn tokens on
---@param tokenType String type of token to spawn, valid values are "damage" and "horror". Other
-- types should use spawnMultipleTokens()
---@param tokenValue Number Value to set the damage/horror to
TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
if tokenValue < 1 or tokenValue > 50 then return end
local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end)
end
TokenManager.spawnResourceCounterToken = function(card, tokenCount)
local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
local rot = card.getRotation()
TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
spawned.call("updateVal", tokenCount)
end)
end
-- Spawns a number of tokens.
---@param tokenType String type of token to spawn, 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
TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown)
if tokenCount < 1 or tokenCount > 12 then
return
end
local offsets = {}
if tokenType == "clue" then
offsets = internal.buildClueOffsets(card, tokenCount)
else
for i = 1, tokenCount do
offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
-- Fix the y-position for the spawn, since positionToWorld considers rotation which can
-- have bad results for face up/down differences
offsets[i].y = card.getPosition().y + 0.15
end
end
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = { }
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset
offsets[i][3] = offsets[i][3] + shiftDown
end
end
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
end
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation())
end
end
-- Spawns a single token at the given global position by copying it from the template bag.
---@param position Global position to spawn the token
---@param tokenType String type of token to spawn, valid values are "damage", "horror",
-- "resource", "doom", or "clue"
---@param rotation Vector Rotation to be used for the new token. Only the y-value will be used,
-- x and z will use the default rotation from the source bag
---@param callback function A callback function triggered after the new token is spawned
TokenManager.spawnToken = function(position, tokenType, rotation, callback)
internal.initTokenTemplates()
local loadTokenType = tokenType
if tokenType == "clue" or tokenType == "doom" then
loadTokenType = "clueDoom"
end
if tokenTemplates[loadTokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local tokenTemplate = tokenTemplates[loadTokenType]
-- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
local rot = Vector(tokenTemplate.Transform.rotX,
270,
tokenTemplate.Transform.rotZ)
if rotation ~= nil then
rot.y = rotation.y
end
if tokenType == "doom" then
rot.z = 180
end
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
})
end
-- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some
-- callers.
---@param card Object Card object to reset the tokens for
TokenManager.resetTokensSpawned = function(card)
tokenSpawnTracker.resetTokensSpawned(card.getGUID())
end
-- Pushes new player card data into the local copy of the Data Helper player data.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addPlayerCardData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
end
end
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addLocationData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
end
-- Checks to see if the given card has location data in the DataHelper
---@param card Object Card to check for data
---@return Boolean True if this card has data in the helper, false otherwise
TokenManager.hasLocationData = function(card)
internal.initDataHelperData()
return internal.getLocationData(card) ~= nil
end
internal.initTokenTemplates = function()
if tokenTemplates ~= nil then
return
end
tokenTemplates = { }
local tokenSource = getObjectFromGUID(TOKEN_SOURCE_GUID)
for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do
local tokenName = tokenTemplate.Memo
tokenTemplates[tokenName] = tokenTemplate
end
end
-- Copies the data from the DataHelper. Will only happen once.
internal.initDataHelperData = function()
if playerCardData ~= nil then
return
end
local dataHelper = getObjectFromGUID(DATA_HELPER_GUID)
playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')
locationData = dataHelper.getTable('LOCATIONS_DATA')
end
-- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card Object Card to maybe spawn tokens for
---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
--- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1
internal.spawnTokensFromUses = function(card, extraUses)
local uses = internal.getUses(card)
if uses == nil then
return
end
local type = nil
local token = nil
local tokenCount = 0
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. This is ugly and duplicated, but impossible to replicate the
-- multi-spawn vs. single spawn otherwise. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if uses.count != nil then
type = cardMetadata.uses.type
token = cardMetadata.uses.token
tokenCount = cardMetadata.uses.count
if extraUses ~= nil and extraUses[type] ~= nil then
tokenCount = tokenCount + extraUses[type]
end
log("Spawning single use tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
TokenManager.spawnTokenGroup(card, token, tokenCount)
else
for i, useInfo in ipairs(uses) do
type = useInfo.type
token = useInfo.token
tokenCount = (useInfo.count or 0)
+ (useInfo.countPerInvestigator or 0) * playArea.getInvestigatorCount()
if extraUses ~= nil and extraUses[type] ~= nil then
tokenCount = tokenCount + extraUses[type]
end
log("Spawning use array tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
-- Shift each spawned group after the first down so they don't pile on each other
TokenManager.spawnTokenGroup(card, token, tokenCount, (i - 1) * 0.8)
end
end
tokenSpawnTracker.markTokensSpawned(card.getGUID())
end
-- Spawn tokens for a card based on the data helper data. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card Object Card to maybe spawn tokens for
internal.spawnTokensFromDataHelper = function(card)
internal.initDataHelperData()
local playerData = internal.getPlayerCardData(card)
if playerData ~= nil then
internal.spawnPlayerCardTokensFromDataHelper(card, playerData)
end
local locationData = internal.getLocationData(card)
if locationData ~= nil then
internal.spawnLocationTokensFromDataHelper(card, locationData)
end
end
-- Spawn tokens for a player card using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
---@param playerData Table Player card data structure retrieved from the DataHelper. Should be
-- the right data for this card.
internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)
token = playerData.tokenType
tokenCount = playerData.tokenCount
log("Spawning data helper tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
TokenManager.spawnTokenGroup(card, token, tokenCount)
tokenSpawnTracker.markTokensSpawned(card.getGUID())
end
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
---@param playerData Table Location data structure retrieved from the DataHelper. Should be
-- the right data for this card.
internal.spawnLocationTokensFromDataHelper = function(card, locationData)
local clueCount = internal.getClueCountFromData(card, locationData)
if clueCount > 0 then
TokenManager.spawnTokenGroup(card, "clue", clueCount)
tokenSpawnTracker.markTokensSpawned(card.getGUID())
end
end
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
end
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
end
internal.getClueCountFromData = function(card, locationData)
-- Return the number of clues to spawn on this location
if locationData == nil then
error('attempted to get clue for unexpected object: ' .. card.getName())
return 0
end
log(card.getName() .. ' : ' .. locationData.type .. ' : ' .. locationData.value .. ' : ' .. locationData.clueSide)
if ((card.is_face_down and locationData.clueSide == 'back')
or (not card.is_face_down and locationData.clueSide == 'front')) then
if locationData.type == 'fixed' then
return locationData.value
elseif locationData.type == 'perPlayer' then
return locationData.value * playArea.getInvestigatorCount()
end
error('unexpected location type: ' .. locationData.type)
end
return 0
end
-- Gets the right uses structure for this card, based on metadata and face up/down state
---@param card Object Card to pull the uses from
internal.getUses = function(card)
local metadata = JSON.decode(card.getGMNotes()) or { }
if metadata.type == "Location" then
if card.is_face_down and metadata.locationBack ~= nil then
return metadata.locationBack.uses
elseif not card.is_face_down and metadata.locationFront ~= nil then
return metadata.locationFront.uses
end
elseif not card.is_face_down then
return metadata.uses
end
return nil
end
-- Dynamically create positions for clues on a card.
---@param card Object Card the clues will be placed on
---@param count Integer How many clues?
---@return Table Array of global positions to spawn the clues at
internal.buildClueOffsets = function(card, count)
local pos = card.getPosition()
local cluePositions = { }
for i = 1, count do
local column = math.floor((i - 1) / 4)
local row = (i - 1) % 4
table.insert(cluePositions, Vector(pos.x - 1 + 0.55 * row, pos.y, pos.z - 1.4 - 0.55 * column))
end
return cluePositions
end
return TokenManager
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local PlayAreaApi = { }
local PLAY_AREA_GUID = "721ba2"
-- Returns the current value of the investigator counter from the playmat
---@return Integer. Number of investigators currently set on the counter
PlayAreaApi.getInvestigatorCount = function()
return getObjectFromGUID(PLAY_AREA_GUID).call("getInvestigatorCount")
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 Color of the player requesting the shift. Used solely to send an error
--- message in the unlikely case that the scripting zone has been deleted
PlayAreaApi.shiftContentsUp = function(playerColor)
return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsUp", playerColor)
end
PlayAreaApi.shiftContentsDown = function(playerColor)
return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsDown", playerColor)
end
PlayAreaApi.shiftContentsLeft = function(playerColor)
return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsLeft", playerColor)
end
PlayAreaApi.shiftContentsRight = function(playerColor)
return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsRight", playerColor)
end
-- Reset the play area's tracking of which cards have had tokens spawned.
PlayAreaApi.resetSpawnedCards = function()
return getObjectFromGUID(PLAY_AREA_GUID).call("resetSpawnedCards")
end
-- Event to be called when the current scenario has changed.
---@param scenarioName Name of the new scenario
PlayAreaApi.onScenarioChanged = function(scenarioName)
getObjectFromGUID(PLAY_AREA_GUID).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 matchTypes Boolean Whether snap points should only snap for the matching card types.
PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
getObjectFromGUID(PLAY_AREA_GUID).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)
getObjectFromGUID(PLAY_AREA_GUID).call("tryObjectEnterContainer",
{ container = container, object = object })
end
return PlayAreaApi
end
end)
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local TokenSpawnTracker = { }
local SPAWN_TRACKER_GUID = "e3ffc9"
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("hasSpawnedTokens", cardGuid)
end
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("markTokensSpawned", cardGuid)
end
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetTokensSpawned", cardGuid)
end
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllAssetAndEvents")
end
TokenSpawnTracker.resetAllLocations = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllLocations")
end
TokenSpawnTracker.resetAll = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAll")
end
return TokenSpawnTracker
end
end)
return __bundle_require("__root")