ah_sce_unpacked/unpacked/Custom_Tile Playermat 3 Green 383d8b.ttslua

1447 lines
46 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)
---------------------------------------------------------
-- specific setup (different for each playmat)
---------------------------------------------------------
PLAYER_COLOR = "Green"
PLAY_ZONE_POSITION = { x = -26.5, y = 4, z = 26.5 }
PLAY_ZONE_SCALE = { x = 32, y = 5, z = 12 }
TRASHCAN_GUID = "5f896a"
STAT_TRACKER_GUID = "af7ed7"
RESOURCE_COUNTER_GUID = "cd15ac"
CLUE_COUNTER_GUID = "032300"
CLUE_CLICKER_GUID = "891403"
require("playermat/Playmat")
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
local tokenManager = require("core/token/TokenManager")
local tokenChecker = require("core/token/TokenChecker")
-- set true to enable debug logging and show Physics.cast()
local DEBUG = false
-- we use this to turn off collision handling until onLoad() is complete
local COLLISION_ENABLED = false
-- position offsets relative to mat [x, y, z]
local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.635}
local DRAWN_CHAOS_TOKEN_OFFSET = {-1.55, 0.5, -0.58}
local DISCARD_BUTTON_OFFSETS = {
{-1.38, 0.1, -0.94},
{-0.92, 0.1, -0.94},
{-0.46, 0.1, -0.94},
{0.00, 0.1, -0.94},
{0.46, 0.1, -0.94},
{0.92, 0.1, -0.94}
}
local MAIN_PLAY_AREA = {
upperLeft = {
x = 1.98,
z = 0.736,
},
lowerRight = {
x = -0.79,
z = -0.39,
}
}
local INVESTIGATOR_AREA = {
upperLeft = {
x = -1.084,
z = 0.06517
},
lowerRight = {
x = -1.258,
z = -0.0805,
}
}
local THREAT_AREA = {
upperLeft = {
x = 1.53,
z = -0.34
},
lowerRight = {
x = -1.13,
z = -0.92,
}
}
local DRAW_DECK_POSITION = { x = -1.82, y = 1, z = 0 }
local DISCARD_PILE_POSITION = { x = -1.82, y = 1.5, z = 0.61 }
local PLAY_ZONE_ROTATION = self.getRotation()
local TRASHCAN
local STAT_TRACKER
local RESOURCE_COUNTER
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
local isDrawButtonVisible = false
function onSave()
return JSON.encode({
zoneID = zoneID,
playerColor = PLAYER_COLOR,
activeInvestigatorId = activeInvestigatorId,
isDrawButtonVisible = isDrawButtonVisible
})
end
function onLoad(save_state)
self.interactable = DEBUG
TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
RESOURCE_COUNTER = getObjectFromGUID(RESOURCE_COUNTER_GUID)
for i = 1, 6 do
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i)
end
self.createButton({
click_function = "drawEncountercard",
function_owner = self,
position = {-1.84, 0, -0.65},
rotation = {0, 80, 0},
width = 265,
height = 190
})
self.createButton({
click_function = "drawChaostokenButton",
function_owner = self,
position = {1.85, 0, -0.74},
rotation = {0, -45, 0},
width = 135,
height = 135
})
self.createButton({
label = "Upkeep",
click_function = "doUpkeep",
function_owner = self,
position = {1.84, 0.1, -0.44},
scale = {0.12, 0.12, 0.12},
width = 800,
height = 280,
font_size = 180
})
local state = JSON.decode(save_state)
if state ~= nil then
zoneID = state.zoneID
PLAYER_COLOR = state.playerColor
activeInvestigatorId = state.activeInvestigatorId
isDrawButtonVisible = state.isDrawButtonVisible
end
showDrawButton(isDrawButtonVisible)
if getObjectFromGUID(zoneID) == nil then spawnDeckZone() end
COLLISION_ENABLED = true
end
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
function log(message)
if DEBUG then print(message) end
end
-- send messages to player who clicked button if no seated player found
function setMessageColor(color)
messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
end
function spawnDeckZone()
spawnObject({
position = self.positionToWorld({-1.4, 0, 0.3 }),
scale = {3, 5, 8 },
type = 'ScriptingTrigger',
callback = function (zone) zoneID = zone.getGUID() end,
callback_owner = self,
rotation = self.getRotation()
})
end
function searchArea(origin, size)
return Physics.cast({
origin = origin,
direction = {0, 1, 0},
orientation = PLAY_ZONE_ROTATION,
type = 3,
size = size,
max_distance = 1,
debug = DEBUG
})
end
function doNotReady(card) return card.getVar("do_not_ready") or false end
---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------
-- builds a function that discards things in searchPosition to discardPosition
-- stuff on the card/deck will be put into the local trashcan
function makeDiscardHandlerFor(searchPosition, discardPosition)
return function ()
for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do
local obj = hitObj.hit_object
if obj.tag == "Deck" or obj.tag == "Card" then
if obj.hasTag("PlayerCard") then
obj.setPositionSmooth(self.positionToWorld(DISCARD_PILE_POSITION), false, true)
obj.setRotation(PLAY_ZONE_ROTATION)
else
obj.setPositionSmooth(discardPosition, false, true)
obj.setRotation({0, -90, 0})
end
-- put chaos tokens back into bag (e.g. Unrelenting)
elseif tokenChecker.isChaosToken(obj) then
local chaosBag = Global.call("findChaosBag")
chaosBag.putObject(obj)
-- don't touch the table or this playmat itself
elseif obj.guid ~= "4ee1f2" and obj ~= self then
TRASHCAN.putObject(obj)
end
end
end
end
-- build a discard button to discard from searchPosition to discardPosition (number must be unique)
function makeDiscardButton(position, discardPosition, number)
local searchPosition = {-position[1], position[2], position[3] + 0.32}
local handler = makeDiscardHandlerFor(searchPosition, discardPosition)
local handlerName = 'handler' .. number
self.setVar(handlerName, handler)
self.createButton({
label = "Discard",
click_function = handlerName,
function_owner = self,
position = {position[1], position[2], position[3] + 0.6},
scale = {0.12, 0.12, 0.12},
width = 900,
height = 350,
font_size = 220
})
end
function findObjectsAtPosition(localPos)
return Physics.cast({
origin = self.positionToWorld(localPos),
direction = {0, 1, 0},
orientation = {0, PLAY_ZONE_ROTATION.y + 90, 0},
type = 3,
size = {3.2, 1, 2},
max_distance = 0,
debug = DEBUG
})
end
---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------
function doUpkeep(_, color, alt_click)
setMessageColor(color)
-- right-click binds to new player color
if alt_click then
PLAYER_COLOR = color
printToColor("Upkeep button bound to " .. color, color)
return
end
local forcedLearning = false
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = v.hit_object
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
elseif obj.tag == "Card" and not obj.is_face_down and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
if not doNotReady(obj) then
obj.setRotation(PLAY_ZONE_ROTATION)
end
if cardMetadata.id == "08031" then
forcedLearning = true
end
if cardMetadata.uses ~= nil then
-- check for cards with 'replenish' in their metadata
local count
local replenish
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if cardMetadata.uses.count ~= nil then
count = cardMetadata.uses.count
replenish = cardMetadata.uses.replenish
else
count = cardMetadata.uses[1].count
replenish = cardMetadata.uses[1].replenish
end
if count and replenish then
replenishTokens(obj, count, replenish)
end
end
end
end
-- flip investigator mini-card and summoned servitor mini-card
-- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)
if activeInvestigatorId ~= nil then
local miniId = string.match(activeInvestigatorId, ".....") .. "-m"
for _, obj in ipairs(getObjects()) do
if obj.tag == "Card" and obj.is_face_down then
local notes = JSON.decode(obj.getGMNotes())
if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
obj.flip()
end
end
end
end
-- gain a resource
RESOURCE_COUNTER.call("addOrSubtract")
-- gain an additional resource for Jenny Barnes
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
RESOURCE_COUNTER.call("addOrSubtract")
printToColor("Gaining 2 resources (Jenny)", messageColor)
end
-- draw a card (with handling for Patrice and Forced Learning)
if activeInvestigatorId == "06005" then
if forcedLearning then
printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
else
local handSize = #Player[PLAYER_COLOR].getHandObjects()
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
end
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
else
drawCardsWithReshuffle(1)
end
end
-- function for "draw 1 button" (that can be added via option panel)
function doDrawOne(_, color)
setMessageColor(color)
drawCardsWithReshuffle(1)
end
-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
getDrawDiscardDecks()
-- Norman Withers handling
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
local harbinger = false
if topCard ~= nil and topCard.getName() == "The Harbinger" then harbinger = true
elseif drawDeck ~= nil and not drawDeck.is_face_down then
local cards = drawDeck.getObjects()
if cards[#cards].name == "The Harbinger" then harbinger = true end
end
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
return
end
if topCard ~= nil then
topCard.deal(numCards, PLAYER_COLOR)
numCards = numCards - 1
if numCards == 0 then return end
end
end
local deckSize = 1
if drawDeck == nil then
deckSize = 0
elseif drawDeck.tag == "Deck" then
deckSize = #drawDeck.getObjects()
end
if deckSize >= numCards then
drawCards(numCards)
return
end
drawCards(deckSize)
if discardPile ~= nil then
shuffleDiscardIntoDeck()
Wait.time(|| drawCards(numCards - deckSize), 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
end
-- get the draw deck and discard pile objects
function getDrawDiscardDecks()
drawDeck = nil
discardPile = nil
topCard = nil
local zone = getObjectFromGUID(zoneID)
if zone == nil then return end
for _, object in ipairs(zone.getObjects()) do
if object.tag == "Deck" or object.tag == "Card" then
if self.positionToLocal(object.getPosition()).z > 0.5 then
discardPile = object
-- Norman Withers handling
elseif string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" and object.tag == "Card" and not object.is_face_down then
topCard = object
else
drawDeck = object
end
end
end
end
function drawCards(numCards)
if drawDeck == nil then return end
drawDeck.deal(numCards, PLAYER_COLOR)
end
function shuffleDiscardIntoDeck()
if not discardPile.is_face_down then discardPile.flip() end
discardPile.shuffle()
discardPile.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)
drawDeck = discardPile
discardPile = nil
end
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
-- replenish Tokens for specific cards (like 'Physical Training (4)')
function replenishTokens(card, count, replenish)
local cardPos = card.getPosition()
-- don't continue for cards on your deck (Norman) or in your discard pile
if self.positionToLocal(cardPos).x < -1 then return end
-- get current amount of resource tokens on the card
local search = searchArea(cardPos, { 2.5, 0.5, 3.5 })
local clickableResourceCounter = nil
local foundTokens = 0
for _, obj in ipairs(search) do
local obj = obj.hit_object
if obj.getCustomObject().image == "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/" then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
elseif obj.getMemo() == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
end
-- handling Runic Axe upgrade sheet for additional replenish
if card.getName() == "Runic Axe" then
for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = v.hit_object
if obj.tag == "Card" then
local notes = JSON.decode(obj.getGMNotes()) or {}
if notes ~= nil and notes.id == "09022-c" then
if obj.getVar("markedBoxes")[7] == 3 then replenish = 2 end
break
end
end
end
end
local newCount = foundTokens + replenish
if newCount > count then newCount = count end
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
tokenManager.spawnTokenGroup(card, "resource", newCount)
end
end
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
if cardMetadata ~= nil and cardMetadata.customizations ~= nil then
for _, collision in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = collision.hit_object
if obj.name == "Card" or obj.name == "CardCustom" then
local notes = JSON.decode(obj.getGMNotes()) or { }
if notes.id == (cardMetadata.id .. "-c") then
for i, customization in ipairs(cardMetadata.customizations) do
if obj.getVar("markedBoxes")[i] == customization.xp
and customization.replaces ~= nil
and customization.replaces.uses ~= nil then
cardMetadata.uses = customization.replaces.uses
card.setGMNotes(JSON.encode(cardMetadata))
end
end
end
end
end
end
end
function spawnTokensFor(object)
local extraUses = { }
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
end
tokenManager.spawnForCard(object, extraUses)
end
function onCollisionEnter(collision_info)
if not COLLISION_ENABLED then return end
local object = collision_info.collision_object
-- only continue for cards
if object.name ~= "Card" and object.name ~= "CardCustom" then return end
maybeUpdateActiveInvestigator(object)
syncCustomizableMetadata(object)
if isInDeckZone(object) then
tokenManager.resetTokensSpawned(object)
elseif shouldSpawnTokens(object) then
spawnTokensFor(object)
end
end
function shouldSpawnTokens(card)
if card.is_face_down then
return false
end
local localCardPos = self.positionToLocal(card.getPosition())
local metadata = JSON.decode(card.getGMNotes())
-- If no metadata we don't know the type, so only spawn in the main area
if metadata == nil then
return inArea(localCardPos, MAIN_PLAY_AREA)
end
-- Spawn tokens for assets and events on the main area, and all encounter types in the threat area
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
or metadata.type == "Enemy"
or metadata.weakness) then
return true
end
return false
end
function onObjectEnterContainer(container, object)
Wait.frames(function() resetTokensIfInDeckZone(container, object) end, 1)
end
function resetTokensIfInDeckZone(container, object)
if isInDeckZone(container) then
tokenManager.resetTokensSpawned(object)
end
end
function isInDeckZone(checkObject)
local deckZone = getObjectFromGUID(zoneID)
if deckZone == nil then
return false
end
for _, obj in ipairs(deckZone.getObjects()) do
if obj == checkObject then
return true
end
end
return false
end
---------------------------------------------------------
-- investigator ID grabbing and skill tracker
---------------------------------------------------------
function maybeUpdateActiveInvestigator(card)
if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end
local notes = JSON.decode(card.getGMNotes())
local class
if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then
if notes.id == activeInvestigatorId then return end
class = notes.class
activeInvestigatorId = notes.id
STAT_TRACKER.call("updateStats", {notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons})
elseif activeInvestigatorId ~= "00000" then
class = "Neutral"
activeInvestigatorId = "00000"
STAT_TRACKER.call("updateStats", {1, 1, 1, 1})
else
return
end
-- change state of action tokens
local search = searchArea(self.positionToWorld({-1.25, 0.05, -1.11}), {4, 1, 1})
local smallToken = nil
local STATE_TABLE = {
["Guardian"] = 1,
["Seeker"] = 2,
["Rogue"] = 3,
["Mystic"] = 4,
["Survivor"] = 5,
["Neutral"] = 6
}
for _, obj in ipairs(search) do
local obj = obj.hit_object
if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
if obj.getScale().x < 0.4 then
smallToken = obj
else
setObjectState(obj, STATE_TABLE[class])
end
end
end
-- update the small token with special action for certain investigators
local SPECIAL_ACTIONS = {
["04002"] = 8, -- Ursula Downs
["01002"] = 9, -- Daisy Walker
["01502"] = 9, -- Daisy Walker
["01002-pb"] = 9, -- Daisy Walker
["06003"] = 10, -- Tony Morgan
["04003"] = 11, -- Finn Edwards
["08016"] = 14 -- Bob Jenkins
}
if smallToken ~= nil then
setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])
end
end
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
end
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
function drawChaostokenButton(_, _, isRightClick)
Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
end
function drawEncountercard(object, player, isRightClick)
Global.call("drawEncountercard", PLAYER_COLOR)
end
function returnGlobalDiscardPosition()
return self.positionToWorld(DISCARD_PILE_POSITION)
end
-- Sets this playermat's draw 1 button to visible
---@param visible Boolean. Whether the draw 1 button should be visible
function showDrawButton(visible)
isDrawButtonVisible = visible
-- create the "Draw 1" button
if isDrawButtonVisible then
self.createButton({
label = "Draw 1",
click_function = "doDrawOne",
function_owner = self,
position = { 1.84, 0.1, -0.36 },
scale = { 0.12, 0.12, 0.12 },
width = 800,
height = 280,
font_size = 180
})
-- remove the "Draw 1" button
else
local buttons = self.getButtons()
for i = 1, #buttons do
if buttons[i].label == "Draw 1" then
self.removeButton(buttons[i].index)
end
end
end
end
-- Spawns / destroys a clickable clue counter for this playmat with the correct amount of clues
---@param showCounter Boolean Whether the clickable clue counter should be present
function clickableClues(showCounter)
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
local clickerPos = CLUE_CLICKER.getPosition()
local clueCount = 0
if showCounter then
-- current clue count
clueCount = CLUE_COUNTER.getVar("exposedValue")
-- remove clues
CLUE_COUNTER.call("removeAllClues")
-- set value for clue clickers
CLUE_CLICKER.call("updateVal", clueCount)
-- move clue counters up
clickerPos.y = 1.52
CLUE_CLICKER.setPosition(clickerPos)
else
-- current clue count
clueCount = CLUE_CLICKER.getVar("val")
-- move clue counters down
clickerPos.y = 1.3
CLUE_CLICKER.setPosition(clickerPos)
-- spawn clues
local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
tokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
end
end
end
-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
CLUE_COUNTER.call("removeAllClues")
CLUE_CLICKER.call("updateVal", 0)
end
-- reports the clue count
---@param useClickableCounters Boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
local count = 0
if useClickableCounters then
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
count = tonumber(CLUE_CLICKER.getVar("val"))
else
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
count = tonumber(CLUE_COUNTER.getVar("exposedValue"))
end
return count
end
-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes
-- is true, the main card slot snap points will only snap assets, while the investigator area point
-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all
-- cards.
---@param matchTypes Boolean. Whether snap points should only snap for the matching card types.
function setLimitSnapsByType(matchTypes)
local snaps = self.getSnapPoints()
for i, snap in ipairs(snaps) do
local snapPos = snap.position
if inArea(snapPos, MAIN_PLAY_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Asset" }
else
table.insert(snaps[i].tags, "Asset")
end
else
snaps[i].tags = nil
end
end
if inArea(snapPos, INVESTIGATOR_AREA) then
local snapTags = snaps[i].tags
if matchTypes then
if snapTags == nil then
snaps[i].tags = { "Investigator" }
else
table.insert(snaps[i].tags, "Investigator")
end
else
snaps[i].tags = nil
end
end
end
self.setSnapPoints(snaps)
end
-- Simple method to check if the given point is in a specified area. Local use only,
---@param point Vector. Point to check, only x and z values are relevant
---@param bounds Table. Defined area to see if the point is within. See MAIN_PLAY_AREA for sample
-- bounds definition.
---@return Boolean. True if the point is in the area defined by bounds
function inArea(point, bounds)
return (point.x < bounds.upperLeft.x
and point.x > bounds.lowerRight.x
and point.z < bounds.upperLeft.z
and point.z > bounds.lowerRight.z)
end
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
end
end)
__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local CHAOS_TOKEN_NAMES = {
["Elder Sign"] = true,
["+1"] = true,
["0"] = true,
["-1"] = true,
["-2"] = true,
["-3"] = true,
["-4"] = true,
["-5"] = true,
["-6"] = true,
["-7"] = true,
["-8"] = true,
["Skull"] = true,
["Cultist"] = true,
["Tablet"] = true,
["Elder Thing"] = true,
["Auto-fail"] = true,
["Bless"] = true,
["Curse"] = true,
["Frost"] = true
}
local TokenChecker = {}
-- returns true if the passed object is a chaos token (by name)
TokenChecker.isChaosToken = function(obj)
if CHAOS_TOKEN_NAMES[obj.getName()] then
return true
else
return false
end
end
return TokenChecker
end
end)
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local 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 row = math.floor(1 + (i - 1) / 4)
local column = (i - 1) % 4
table.insert(cluePositions, Vector(pos.x + 1.5 - 0.55 * row, pos.y + 0.15, pos.z - 0.825 + 0.55 * column))
end
return cluePositions
end
return TokenManager
end
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")