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

1741 lines
56 KiB
Plaintext
Raw Normal View History

2022-12-13 14:02:30 -05:00
-- Bundled by luabundle {"version":"1.6.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
local loadingPlaceholder = {[{}] = true}
local register
local modules = {}
local require
local loaded = {}
register = function(name, body)
if not modules[name] then
modules[name] = body
end
end
require = function(name)
local loadedModule = loaded[name]
if loadedModule then
if loadedModule == loadingPlaceholder then
return nil
end
else
if not modules[name] then
if not superRequire then
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
error('Tried to require ' .. identifier .. ', but no such module has been registered')
else
return superRequire(name)
end
end
loaded[name] = loadingPlaceholder
loadedModule = modules[name](require, loaded, register, modules)
loaded[name] = loadedModule
end
return loadedModule
end
return require, loaded, register, modules
end)(nil)
2023-08-27 21:09:46 -04:00
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
---------------------------------------------------------
-- specific setup (different for each playmat)
---------------------------------------------------------
TRASHCAN_GUID = "4b8594"
STAT_TRACKER_GUID = "e74881"
RESOURCE_COUNTER_GUID = "a4b60d"
CLUE_COUNTER_GUID = "37be78"
CLUE_CLICKER_GUID = "4111de"
require("playermat/Playmat")
end)
2022-12-13 14:02:30 -05:00
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
2023-01-29 19:31:52 -05:00
local tokenManager = require("core/token/TokenManager")
local tokenChecker = require("core/token/TokenChecker")
2023-08-27 21:09:46 -04:00
local navigationOverlayApi = require("core/NavigationOverlayApi")
2023-01-29 19:31:52 -05:00
2022-12-13 14:02:30 -05:00
-- 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
2023-04-22 16:56:01 -04:00
local collisionEnabled = false
2022-12-13 14:02:30 -05:00
-- position offsets relative to mat [x, y, z]
2023-04-22 16:56:01 -04:00
local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.625}
local DRAWN_CHAOS_TOKEN_OFFSET = {-1.55, 0.25, -0.58}
-- x-Values for discard buttons
local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91}
2022-10-19 19:07:47 -04:00
2023-08-27 21:09:46 -04:00
local SEARCH_AROUND_SELF_X_BUFFER = 8
2023-04-22 16:56:01 -04:00
-- defined areas for the function "inArea()""
2023-01-29 19:31:52 -05:00
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 }
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
local TRASHCAN
local STAT_TRACKER
local RESOURCE_COUNTER
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
2023-01-29 19:31:52 -05:00
local isDrawButtonVisible = false
2023-04-22 16:56:01 -04:00
-- global variable to report "Dream-Enhancing Serum" status
isDES = false
2023-01-29 19:31:52 -05:00
function onSave()
return JSON.encode({
zoneID = zoneID,
2023-04-22 16:56:01 -04:00
playerColor = playerColor,
2023-01-29 19:31:52 -05:00
activeInvestigatorId = activeInvestigatorId,
isDrawButtonVisible = isDrawButtonVisible
})
end
2022-10-19 19:07:47 -04:00
2022-12-13 14:02:30 -05:00
function onLoad(save_state)
2020-12-06 09:42:32 -05:00
self.interactable = DEBUG
2022-12-13 14:02:30 -05:00
TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
RESOURCE_COUNTER = getObjectFromGUID(RESOURCE_COUNTER_GUID)
2023-04-22 16:56:01 -04:00
-- button creation
2022-12-13 14:02:30 -05:00
for i = 1, 6 do
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i)
2020-12-06 09:42:32 -05:00
end
self.createButton({
click_function = "drawEncountercard",
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {-1.84, 0, -0.65},
rotation = {0, 80, 0},
width = 265,
height = 190
2020-12-06 09:42:32 -05:00
})
self.createButton({
2023-04-22 16:56:01 -04:00
click_function = "drawChaosTokenButton",
2020-12-06 09:42:32 -05:00
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {1.85, 0, -0.74},
rotation = {0, -45, 0},
width = 135,
height = 135
2020-12-06 09:42:32 -05:00
})
self.createButton({
2022-12-13 14:02:30 -05:00
label = "Upkeep",
2021-09-03 13:52:12 -04:00
click_function = "doUpkeep",
function_owner = self,
2022-12-13 14:02:30 -05:00
position = {1.84, 0.1, -0.44},
2021-09-03 13:52:12 -04:00
scale = {0.12, 0.12, 0.12},
width = 800,
height = 280,
font_size = 180
})
2023-04-22 16:56:01 -04:00
-- save state loading
2020-12-06 09:42:32 -05:00
local state = JSON.decode(save_state)
2021-02-13 12:12:29 -05:00
if state ~= nil then
2022-12-13 14:02:30 -05:00
zoneID = state.zoneID
2023-04-22 16:56:01 -04:00
playerColor = state.playerColor
2022-12-13 14:02:30 -05:00
activeInvestigatorId = state.activeInvestigatorId
2023-01-29 19:31:52 -05:00
isDrawButtonVisible = state.isDrawButtonVisible
2020-12-06 09:42:32 -05:00
end
2023-01-29 19:31:52 -05:00
showDrawButton(isDrawButtonVisible)
2022-12-13 14:02:30 -05:00
if getObjectFromGUID(zoneID) == nil then spawnDeckZone() end
2023-04-22 16:56:01 -04:00
collisionEnabled = true
math.randomseed(os.time())
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
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()
})
2021-09-03 13:52:12 -04:00
end
2022-12-13 14:02:30 -05:00
function searchArea(origin, size)
return Physics.cast({
origin = origin,
direction = {0, 1, 0},
2023-04-22 16:56:01 -04:00
orientation = self.getRotation(),
2022-12-13 14:02:30 -05:00
type = 3,
size = size,
max_distance = 1,
debug = DEBUG
})
end
2022-10-19 19:07:47 -04:00
2023-08-27 21:09:46 -04:00
-- Finds all objects on the playmat and associated set aside zone.
function searchAroundSelf()
local bounds = self.getBoundsNormalized()
-- Increase the width to cover the set aside zone
bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER
-- Since the cast is centered on the position, shift left or right to keep the non-set aside edge
-- of the cast at the edge of the playmat
-- setAsideDirection accounts for the set aside zone being on the left or right, depending on the
-- table position of the playmat
local setAsideDirection = bounds.center.z > 0 and 1 or -1
local localCenter = self.positionToLocal(bounds.center)
localCenter.x = localCenter.x
+ setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
return searchArea(self.positionToWorld(localCenter), bounds.size)
end
function findCardsAroundSelf()
local cards = { }
for _, collision in ipairs(searchAroundSelf()) do
local obj = collision.hit_object
if obj.name == "Card" or obj.name == "CardCustom" then
table.insert(cards, obj)
end
end
return cards
end
2023-04-22 16:56:01 -04:00
function doNotReady(card)
return card.getVar("do_not_ready") or false
end
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
-- 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
2023-01-29 19:31:52 -05:00
obj.setPositionSmooth(self.positionToWorld(DISCARD_PILE_POSITION), false, true)
2023-04-22 16:56:01 -04:00
obj.setRotation(self.getRotation())
2022-12-13 14:02:30 -05:00
else
obj.setPositionSmooth(discardPosition, false, true)
obj.setRotation({0, -90, 0})
end
2023-01-29 19:31:52 -05:00
-- put chaos tokens back into bag (e.g. Unrelenting)
elseif tokenChecker.isChaosToken(obj) then
local chaosBag = Global.call("findChaosBag")
chaosBag.putObject(obj)
2022-12-13 14:02:30 -05:00
-- don't touch the table or this playmat itself
elseif obj.guid ~= "4ee1f2" and obj ~= self then
TRASHCAN.putObject(obj)
end
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
end
2021-11-16 23:41:43 -05:00
end
2021-09-03 13:52:12 -04:00
2022-12-13 14:02:30 -05:00
-- build a discard button to discard from searchPosition to discardPosition (number must be unique)
2023-04-22 16:56:01 -04:00
function makeDiscardButton(xValue, discardPosition, number)
local position = { xValue, 0.1, -0.94}
2022-12-13 14:02:30 -05:00
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,
scale = {0.12, 0.12, 0.12},
2023-01-29 19:31:52 -05:00
width = 900,
height = 350,
font_size = 220
2022-12-13 14:02:30 -05:00
})
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
function findObjectsAtPosition(localPos)
return Physics.cast({
origin = self.positionToWorld(localPos),
direction = {0, 1, 0},
2023-04-22 16:56:01 -04:00
orientation = {0, self.getRotation().y + 90, 0},
2022-12-13 14:02:30 -05:00
type = 3,
size = {3.2, 1, 2},
max_distance = 0,
debug = DEBUG
})
2021-04-10 00:44:08 -04:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
-- calls the Upkeep function with correct parameter
function doUpkeepFromHotkey(color)
doUpkeep(_, color)
end
2020-12-06 09:42:32 -05:00
2023-04-22 16:56:01 -04:00
function doUpkeep(_, clickedByColor, isRightClick)
-- right-click allow color changing
if isRightClick then
changeColor(clickedByColor)
2022-12-13 14:02:30 -05:00
return
end
2020-12-06 09:42:32 -05:00
2023-04-22 16:56:01 -04:00
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or clickedByColor
2022-12-13 14:02:30 -05:00
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
2023-04-22 16:56:01 -04:00
local forcedLearning = false
2023-08-27 21:09:46 -04:00
local rot = self.getRotation()
for _, v in ipairs(searchAroundSelf()) do
2022-12-13 14:02:30 -05:00
local obj = v.hit_object
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
2023-08-27 21:09:46 -04:00
elseif obj.tag == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
2022-12-13 14:02:30 -05:00
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
2023-01-29 19:31:52 -05:00
if not doNotReady(obj) then
2023-08-27 21:09:46 -04:00
local cardRotation = round(obj.getRotation().y, 0) - rot.y
local yRotDiff = 0
if cardRotation < 0 then
cardRotation = cardRotation + 360
end
-- rotate cards to the next multiple of 90° towards 0°
if cardRotation > 90 and cardRotation <= 180 then
yRotDiff = 90
elseif cardRotation < 270 and cardRotation > 180 then
yRotDiff = 270
end
-- set correct rotation for face-down cards
rot.z = obj.is_face_down and 180 or 0
obj.setRotation({rot.x, rot.y + yRotDiff, rot.z})
2022-12-13 14:02:30 -05:00
end
if cardMetadata.id == "08031" then
forcedLearning = true
end
if cardMetadata.uses ~= nil then
2023-08-27 21:09:46 -04:00
tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)
2022-12-13 14:02:30 -05:00
end
end
end
2022-03-27 10:12:31 -04:00
2023-01-29 19:31:52 -05:00
-- flip investigator mini-card and summoned servitor mini-card
-- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)
2022-12-13 14:02:30 -05:00
if activeInvestigatorId ~= nil then
2023-01-29 19:31:52 -05:00
local miniId = string.match(activeInvestigatorId, ".....") .. "-m"
2022-12-13 14:02:30 -05:00
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()
2022-03-27 10:12:31 -04:00
end
2022-12-13 14:02:30 -05:00
end
2022-03-27 10:12:31 -04:00
end
2022-12-13 14:02:30 -05:00
end
2022-03-27 10:12:31 -04:00
2023-04-22 16:56:01 -04:00
-- gain a resource (or two if playing Jenny Barnes)
2022-12-13 14:02:30 -05:00
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
2023-04-22 16:56:01 -04:00
gainResources(2)
2022-12-13 14:02:30 -05:00
printToColor("Gaining 2 resources (Jenny)", messageColor)
2023-04-22 16:56:01 -04:00
else
gainResources(1)
2022-12-13 14:02:30 -05:00
end
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
-- draw a card (with handling for Patrice and Forced Learning)
if activeInvestigatorId == "06005" then
if forcedLearning then
printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
else
2023-04-22 16:56:01 -04:00
local handSize = #Player[playerColor].getHandObjects()
2022-12-13 14:02:30 -05:00
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
2023-08-27 21:09:46 -04:00
elseif activeInvestigatorId == "89001" then
printToColor("Drawing 2 cards (Subject 5U-21)", messageColor)
drawCardsWithReshuffle(2)
2022-12-13 14:02:30 -05:00
else
drawCardsWithReshuffle(1)
end
2020-12-06 09:42:32 -05:00
end
2023-04-22 16:56:01 -04:00
-- adds the specified amount of resources to the resource counter
function gainResources(amount)
local count = RESOURCE_COUNTER.getVar("val")
local add = tonumber(amount) or 0
RESOURCE_COUNTER.call("updateVal", count + add)
end
2023-01-29 19:31:52 -05:00
-- function for "draw 1 button" (that can be added via option panel)
2022-12-13 14:02:30 -05:00
function doDrawOne(_, color)
2023-04-22 16:56:01 -04:00
-- send messages to player who clicked button if no seated player found
messageColor = Player[playerColor].seated and playerColor or color
2022-12-13 14:02:30 -05:00
drawCardsWithReshuffle(1)
2021-11-16 23:41:43 -05:00
end
2022-12-13 14:02:30 -05:00
-- 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
2021-11-16 23:41:43 -05:00
2022-12-13 14:02:30 -05:00
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
return
end
2021-11-16 23:41:43 -05:00
2022-12-13 14:02:30 -05:00
if topCard ~= nil then
2023-04-22 16:56:01 -04:00
topCard.deal(numCards, playerColor)
2022-12-13 14:02:30 -05:00
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
2021-11-16 23:41:43 -05:00
2022-12-13 14:02:30 -05:00
drawCards(deckSize)
if discardPile ~= nil then
shuffleDiscardIntoDeck()
Wait.time(|| drawCards(numCards - deckSize), 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
-- 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
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
function drawCards(numCards)
if drawDeck == nil then return end
2023-04-22 16:56:01 -04:00
drawDeck.deal(numCards, playerColor)
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
function shuffleDiscardIntoDeck()
if not discardPile.is_face_down then discardPile.flip() end
discardPile.shuffle()
2023-01-29 19:31:52 -05:00
discardPile.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)
2022-12-13 14:02:30 -05:00
drawDeck = discardPile
discardPile = nil
2020-12-06 09:42:32 -05:00
end
2023-04-22 16:56:01 -04:00
-- discard a random non-hidden card from hand
function doDiscardOne()
local hand = Player[playerColor].getHandObjects()
if #hand == 0 then
broadcastToAll("Cannot discard from empty hand!", "Red")
else
local choices = {}
for i = 1, #hand do
local notes = JSON.decode(hand[i].getGMNotes())
if notes ~= nil then
if notes.hidden ~= true then
table.insert(choices, i)
end
else
table.insert(choices, i)
end
end
if #choices == 0 then
broadcastToAll("Hidden cards can't be randomly discarded.", "Orange")
return
end
-- get a random non-hidden card (from the "choices" table)
local num = math.random(1, #choices)
hand[choices[num]].setPosition(returnGlobalDiscardPosition())
broadcastToAll(playerColor .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White")
end
end
---------------------------------------------------------
-- color related functions
---------------------------------------------------------
-- changes the player color
function changeColor(clickedByColor)
local colorList = {
"White",
"Brown",
"Red",
"Orange",
"Yellow",
"Green",
"Teal",
"Blue",
"Purple",
"Pink"
}
-- remove existing colors from the list of choices
for _, existingColor in ipairs(Player.getAvailableColors()) do
for i, newColor in ipairs(colorList) do
if existingColor == newColor then
table.remove(colorList, i)
end
end
end
-- show the option dialog for color selection to the player that triggered this
Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color)
local HAND_ZONE_GUIDS = {
"a70eee", -- White
"5fe087", -- Orange
"0285cc", -- Green
"be2f17" -- Red
}
local index
local startPos = self.getPosition()
-- get respective hand zone by position
if startPos.x < -42 then
if startPos.z > 0 then
index = 1
else
index = 2
end
else
if startPos.z > 0 then
index = 3
else
index = 4
end
end
-- update the color of the hand zone
local handZone = getObjectFromGUID(HAND_ZONE_GUIDS[index])
handZone.setValue(color)
-- if the seated player clicked this, reseat him to the new color
if clickedByColor == playerColor then
2023-08-27 21:09:46 -04:00
navigationOverlayApi.copyVisibility(playerColor, color)
2023-04-22 16:56:01 -04:00
Player[playerColor].changeColor(color)
end
-- update the internal variable
playerColor = color
end)
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
2023-08-27 21:09:46 -04:00
-- Finds all customizable cards in this play area and updates their metadata based on the selections
-- on the matching upgrade sheet.
-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be
-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the
-- number of customizable cards in play.
function syncAllCustomizableCards()
for _, card in ipairs(findCardsAroundSelf()) do
syncCustomizableMetadata(card)
2023-01-29 19:31:52 -05:00
end
2020-12-06 09:42:32 -05:00
end
2023-01-29 19:31:52 -05:00
function syncCustomizableMetadata(card)
local cardMetadata = JSON.decode(card.getGMNotes()) or { }
2023-08-27 21:09:46 -04:00
if cardMetadata == nil or cardMetadata.customizations == nil then
return
end
for _, upgradeSheet in ipairs(findCardsAroundSelf()) do
local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { }
if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then
for i, customization in ipairs(cardMetadata.customizations) do
if customization.replaces ~= nil and customization.replaces.uses ~= nil then
-- Allowed use of call(), no APIs for individual cards
if upgradeSheet.call("isUpgradeActive", i) then
cardMetadata.uses = customization.replaces.uses
card.setGMNotes(JSON.encode(cardMetadata))
else
-- TODO: Get the original metadata to restore it... maybe. This should only be
-- necessary in the very unlikely case that a user un-checks a previously-full upgrade
-- row while the card is in play. It will be much easier once the AllPlayerCardsApi is
-- in place, so defer until it is
2022-12-13 14:02:30 -05:00
end
end
end
2022-10-19 19:07:47 -04:00
end
2020-12-06 09:42:32 -05:00
end
end
2023-01-29 19:31:52 -05:00
function spawnTokensFor(object)
local extraUses = { }
if activeInvestigatorId == "03004" then
extraUses["Charge"] = 1
2022-12-13 14:02:30 -05:00
end
2020-12-06 09:42:32 -05:00
2023-01-29 19:31:52 -05:00
tokenManager.spawnForCard(object, extraUses)
2020-12-06 09:42:32 -05:00
end
function onCollisionEnter(collision_info)
2022-12-13 14:02:30 -05:00
local object = collision_info.collision_object
2023-04-22 16:56:01 -04:00
-- detect if "Dream-Enhancing Serum" is placed
if object.getName() == "Dream-Enhancing Serum" then isDES = true end
-- only continue if loading is completed
if not collisionEnabled then return end
2022-12-13 14:02:30 -05:00
-- only continue for cards
if object.name ~= "Card" and object.name ~= "CardCustom" then return end
2023-01-29 19:31:52 -05:00
2022-12-13 14:02:30 -05:00
maybeUpdateActiveInvestigator(object)
2023-01-29 19:31:52 -05:00
syncCustomizableMetadata(object)
2022-12-13 14:02:30 -05:00
2023-01-29 19:31:52 -05:00
if isInDeckZone(object) then
tokenManager.resetTokensSpawned(object)
2023-04-22 16:56:01 -04:00
removeTokensFromObject(object)
2023-01-29 19:31:52 -05:00
elseif shouldSpawnTokens(object) then
2020-12-06 09:42:32 -05:00
spawnTokensFor(object)
end
end
2023-04-22 16:56:01 -04:00
-- detect if "Dream-Enhancing Serum" is removed
function onCollisionExit(collision_info)
if collision_info.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end
end
-- checks if tokens should be spawned for the provided card
2023-01-29 19:31:52 -05:00
function shouldSpawnTokens(card)
if card.is_face_down then
return false
end
2023-08-27 21:09:46 -04:00
2023-01-29 19:31:52 -05:00
local localCardPos = self.positionToLocal(card.getPosition())
local metadata = JSON.decode(card.getGMNotes())
2023-04-22 16:56:01 -04:00
2023-01-29 19:31:52 -05:00
-- 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
2023-04-22 16:56:01 -04:00
-- Spawn tokens for assets and events on the main area
2023-01-29 19:31:52 -05:00
if inArea(localCardPos, MAIN_PLAY_AREA)
and (metadata.type == "Asset"
or metadata.type == "Event") then
return true
end
2023-04-22 16:56:01 -04:00
-- Spawn tokens for all encounter types in the threat area
2023-01-29 19:31:52 -05:00
if inArea(localCardPos, THREAT_AREA)
and (metadata.type == "Treachery"
or metadata.type == "Enemy"
or metadata.weakness) then
return true
end
2023-04-22 16:56:01 -04:00
2023-01-29 19:31:52 -05:00
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)
2023-04-22 16:56:01 -04:00
removeTokensFromObject(container)
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- checks if an object is in this mats deckzone
2023-01-29 19:31:52 -05:00
function isInDeckZone(checkObject)
local deckZone = getObjectFromGUID(zoneID)
if deckZone == nil then
return false
end
2023-04-22 16:56:01 -04:00
2023-01-29 19:31:52 -05:00
for _, obj in ipairs(deckZone.getObjects()) do
if obj == checkObject then
return true
end
end
return false
end
2023-04-22 16:56:01 -04:00
-- removes tokens from the provided card/deck
function removeTokensFromObject(object)
for _, v in ipairs(searchArea(object.getPosition(), { 3, 1, 4 })) do
local obj = v.hit_object
if obj.getGUID() ~= "4ee1f2" and -- table
obj ~= self and
obj.type ~= "Deck" and
obj.type ~= "Card" and
obj.memo ~= nil and
obj.getLock() == false and
obj.getDescription() ~= "Action Token" and
not tokenChecker.isChaosToken(obj) then
TRASHCAN.putObject(obj)
end
end
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
-- investigator ID grabbing and skill tracker
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2022-10-19 19:07:47 -04:00
function maybeUpdateActiveInvestigator(card)
2023-01-29 19:31:52 -05:00
if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end
2022-12-13 14:02:30 -05:00
local notes = JSON.decode(card.getGMNotes())
2023-01-29 19:31:52 -05:00
local class
if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then
if notes.id == activeInvestigatorId then return end
class = notes.class
2022-12-13 14:02:30 -05:00
activeInvestigatorId = notes.id
STAT_TRACKER.call("updateStats", {notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons})
2023-01-29 19:31:52 -05:00
elseif activeInvestigatorId ~= "00000" then
class = "Neutral"
activeInvestigatorId = "00000"
STAT_TRACKER.call("updateStats", {1, 1, 1, 1})
else
return
end
2022-12-13 14:02:30 -05:00
2023-01-29 19:31:52 -05:00
-- change state of action tokens
local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1})
local smallToken = nil
local STATE_TABLE = {
["Guardian"] = 1,
["Seeker"] = 2,
["Rogue"] = 3,
["Mystic"] = 4,
["Survivor"] = 5,
["Neutral"] = 6
}
2022-12-13 14:02:30 -05:00
2023-01-29 19:31:52 -05:00
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])
2022-12-13 14:02:30 -05:00
end
end
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2023-01-29 19:31:52 -05:00
-- 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])
2022-10-19 19:07:47 -04:00
end
end
2022-12-13 14:02:30 -05:00
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
2022-10-19 19:07:47 -04:00
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
function drawChaosTokenButton(_, _, isRightClick)
Global.call("drawChaosToken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
2020-12-06 09:42:32 -05:00
end
2022-12-13 14:02:30 -05:00
function drawEncountercard(_, _, isRightClick)
Global.call("drawEncountercard", {self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET), self.getRotation(), isRightClick})
2020-12-06 09:42:32 -05:00
end
2023-01-29 19:31:52 -05:00
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
2023-04-22 16:56:01 -04:00
tokenManager.spawnToken(pos, "clue", self.getRotation())
2023-01-29 19:31:52 -05:00
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)
2021-01-02 22:49:38 -05:00
end
2022-12-13 14:02:30 -05:00
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
2021-01-02 22:49:38 -05:00
function updatePlayerCards(args)
2023-01-29 19:31:52 -05:00
local customDataHelper = getObjectFromGUID(args[1])
local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
tokenManager.addPlayerCardData(playerCardData)
end
2023-08-27 21:09:46 -04:00
-- utility function for rounding
---@param num Number Initial value
---@param numDecimalPlaces Number Amount of decimal places
function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
end)
__bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local NavigationOverlayApi = {}
local HANDLER_GUID = "797ede"
-- Copies the visibility for the Navigation overlay
---@param startColor String Color of the player to copy from
---@param targetColor String Color of the targeted player
NavigationOverlayApi.copyVisibility = function(startColor, targetColor)
getObjectFromGUID(HANDLER_GUID).call("copyVisibility", {
startColor = startColor,
targetColor = targetColor
})
end
-- Changes the Navigation Overlay view ("Full View" --> "Play Areas" --> "Closed" etc.)
---@param playerColor String Color of the player to update the visibility for
NavigationOverlayApi.cycleVisibility = function(playerColor)
getObjectFromGUID(HANDLER_GUID).call("cycleVisibility", playerColor)
end
return NavigationOverlayApi
end
2023-01-29 19:31:52 -05:00
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)
}
}
2023-08-27 21:09:46 -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
}
2023-01-29 19:31:52 -05:00
-- 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
2023-08-27 21:09:46 -04:00
---@param tokenType String Type of token to spawn, valid values are "damage", "horror",
2023-01-29 19:31:52 -05:00
-- "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
2023-08-27 21:09:46 -04:00
---@param subType Number Subtype of token to spawn. This will only differ from the tokenName for resource tokens
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)
2023-01-29 19:31:52 -05:00
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
2023-08-27 21:09:46 -04:00
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)
2023-01-29 19:31:52 -05:00
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
2023-08-27 21:09:46 -04:00
---@param subType Number Subtype of token to spawn. This will only differ from the tokenName for resource tokens
TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)
2023-01-29 19:31:52 -05:00
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
2023-08-27 21:09:46 -04:00
-- handling for not provided subtype (for example when spawning from custom data helpers)
if subType == nil then
subType = ""
end
-- 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
for i = 1, tokenCount do
2023-08-27 21:09:46 -04:00
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
end
2023-01-29 19:31:52 -05:00
-- 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
2023-08-27 21:09:46 -04:00
-- Checks a card for metadata to maybe replenish it
---@param card Object Card object to be replenished
---@param uses Table The already decoded metadata.uses (to avoid decoding again)
---@param mat 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)
end
end
2023-01-29 19:31:52 -05:00
-- 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)
2023-08-27 21:09:46 -04:00
if uses == nil then return end
-- go through tokens to spawn
local type, token, tokenCount
for i, useInfo in ipairs(uses) do
type = useInfo.type
token = useInfo.token
tokenCount = (useInfo.count or 0)
+ (useInfo.countPerInvestigator or 0) * playArea.getInvestigatorCount()
2023-01-29 19:31:52 -05:00
if extraUses ~= nil and extraUses[type] ~= nil then
tokenCount = tokenCount + extraUses[type]
end
2023-08-27 21:09:46 -04:00
-- 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, type)
2023-01-29 19:31:52 -05:00
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
2023-08-27 21:09:46 -04:00
--log("Spawning data helper tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
2023-01-29 19:31:52 -05:00
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
2023-08-27 21:09:46 -04:00
--log(card.getName() .. ' : ' .. locationData.type .. ' : ' .. locationData.value .. ' : ' .. locationData.clueSide)
2023-01-29 19:31:52 -05: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 * 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
2023-08-27 21:09:46 -04:00
---@param card Object Card object to be replenished
---@param uses Table The already decoded metadata.uses (to avoid decoding again)
---@param mat Object The playmat the card is placed on (for rotation and casting)
internal.replenishTokens = function(card, uses, mat)
local cardPos = card.getPosition()
-- don't continue for cards on the deck (Norman) or in the discard pile
if mat.positionToLocal(cardPos).x < -1 then return end
-- get current amount of resource tokens on the card
local search = internal.searchOnCard(cardPos, card.getRotation())
local clickableResourceCounter = nil
local foundTokens = 0
for _, obj in ipairs(search) do
local obj = obj.hit_object
local memo = obj.getMemo()
if (stateTable[memo] or 0) > 0 then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
elseif memo == "resourceCounter" then
foundTokens = obj.getVar("val")
clickableResourceCounter = obj
break
end
end
-- 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
2023-01-29 19:31:52 -05:00
2023-08-27 21:09:46 -04:00
-- update the clickable counter or spawn a group of tokens
if clickableResourceCounter then
clickableResourceCounter.call("updateVal", newCount)
else
TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)
end
end
-- searches on a card (standard size) and returns the result
---@param position Table Position of the card
---@param rotation Table Rotation of the card
internal.searchOnCard = function(position, rotation)
return Physics.cast({
origin = position,
direction = {0, 1, 0},
orientation = rotation,
type = 3,
size = { 2.5, 0.5, 3.5 },
max_distance = 1,
debug = false
})
end
return TokenManager
2023-01-29 19:31:52 -05:00
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local PlayAreaApi = { }
local PLAY_AREA_GUID = "721ba2"
2023-08-27 21:09:46 -04:00
local IMAGE_SWAPPER = "b7b45b"
2023-01-29 19:31:52 -05:00
-- 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
2023-08-27 21:09:46 -04:00
-- Updates the current value of the investigator counter from the playmat
---@param count Number of investigators to set on the counter
PlayAreaApi.setInvestigatorCount = function(count)
return getObjectFromGUID(PLAY_AREA_GUID).call("setInvestigatorCount", count)
end
2023-01-29 19:31:52 -05:00
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
-- fixed objects will be ignored, as will anything the player has tagged with
-- 'displacement_excluded'
---@param playerColor 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
2023-04-22 16:56:01 -04:00
-- counts the VP on locations in the play area
PlayAreaApi.countVP = function()
return getObjectFromGUID(PLAY_AREA_GUID).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 getObjectFromGUID(PLAY_AREA_GUID).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 getObjectFromGUID(PLAY_AREA_GUID).call("highlightCountedVP", state)
end
-- Checks if an object is in the play area (returns true or false)
PlayAreaApi.isInPlayArea = function(object)
return getObjectFromGUID(PLAY_AREA_GUID).call("isInPlayArea", object)
end
2023-08-27 21:09:46 -04:00
PlayAreaApi.getSurface = function()
return getObjectFromGUID(PLAY_AREA_GUID).getCustomObject().image
end
PlayAreaApi.updateSurface = function(url)
return getObjectFromGUID(IMAGE_SWAPPER).call("updateSurface", url)
end
2023-01-29 19:31:52 -05:00
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
2022-12-13 14:02:30 -05:00
end
end)
return __bundle_require("__root")