ah_sce_unpacked/unpacked.ttslua

2057 lines
70 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}
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
local register
local modules = {}
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
local require
local loaded = {}
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
register = function(name, body)
if not modules[name] then
modules[name] = body
end
end
2020-12-06 09:42:32 -05:00
2022-12-13 14:02:30 -05:00
require = function(name)
local loadedModule = loaded[name]
2021-03-23 10:59:55 -04:00
2022-12-13 14:02:30 -05:00
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
2020-11-28 13:23:58 -05:00
2022-12-13 14:02:30 -05:00
loaded[name] = loadingPlaceholder
loadedModule = modules[name](require, loaded, register, modules)
loaded[name] = loadedModule
end
return loadedModule
end
2021-03-23 10:59:55 -04:00
2022-12-13 14:02:30 -05:00
return require, loaded, register, modules
end)(nil)
2023-04-22 16:56:01 -04:00
__bundle_register("chaosbag/BlessCurseManagerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
2023-01-29 19:31:52 -05:00
do
2023-04-22 16:56:01 -04:00
local BlessCurseManagerApi = {}
local MANAGER_GUID = "5933fb"
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- removes all taken tokens and resets the counts
BlessCurseManagerApi.removeTakenTokensAndReset = function()
local BlessCurseManager = getObjectFromGUID(MANAGER_GUID)
Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Bless") end, 0.05)
Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Curse") end, 0.10)
Wait.time(function() BlessCurseManager.call("doReset", "White") end, 0.15)
end
-- updates the internal count (called by cards that seal bless/curse tokens)
BlessCurseManagerApi.sealedToken = function(type, guid)
getObjectFromGUID(MANAGER_GUID).call("sealedToken", { type = type, guid = guid })
end
-- updates the internal count (called by cards that seal bless/curse tokens)
BlessCurseManagerApi.releasedToken = function(type, guid)
getObjectFromGUID(MANAGER_GUID).call("releasedToken", { type = type, guid = guid })
end
-- broadcasts the current status for bless/curse tokens
---@param playerColor String Color of the player to show the broadcast to
BlessCurseManagerApi.broadcastStatus = function(playerColor)
getObjectFromGUID(MANAGER_GUID).call("broadcastStatus", playerColor)
end
-- adds Wendy's menu to the hovered card (allows sealing of tokens)
---@param color String Color of the player to show the broadcast to
BlessCurseManagerApi.addWendysMenu = function(playerColor, hoveredObject)
getObjectFromGUID(MANAGER_GUID).call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject })
end
return BlessCurseManagerApi
end
end)
__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local MythosAreaApi = {}
local MYTHOS_AREA_GUID = "9f334f"
-- returns the chaos token metadata (if provided through scenario reference card)
MythosAreaApi.returnTokenData = function()
return getObjectFromGUID("9f334f").call("returnTokenData")
end
return MythosAreaApi
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
-- 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
return PlayAreaApi
end
end)
__bundle_register("core/SoundCubeApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local SoundCubeApi = {}
-- this table links the name of a trigger effect to its index
local soundIndices = {
["Vacuum"] = 0,
["Deep Bell"] = 1,
["Dark Souls"] = 2
2023-01-29 19:31:52 -05:00
}
2023-04-22 16:56:01 -04:00
function playTriggerEffect(index)
getObjectsWithTag("SoundCube")[1].AssetBundle.playTriggerEffect(index)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- plays the by name requested sound
---@param soundName String Name of the sound to play
SoundCubeApi.playSoundByName = function(soundName)
playTriggerEffect(soundIndices[soundName])
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
return SoundCubeApi
end
end)
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local TokenSpawnTracker = { }
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local SPAWN_TRACKER_GUID = "e3ffc9"
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("hasSpawnedTokens", cardGuid)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.markTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("markTokensSpawned", cardGuid)
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetTokensSpawned", cardGuid)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.resetAllAssetAndEvents = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllAssetAndEvents")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.resetAllLocations = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllLocations")
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
TokenSpawnTracker.resetAll = function()
return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAll")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return TokenSpawnTracker
end
end)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("core/Global")
end)
__bundle_register("core/Global", function(require, _LOADED, __bundle_register, __bundle_modules)
---------------------------------------------------------
-- general setup
---------------------------------------------------------
ENCOUNTER_DECK_POS = {-3.93, 1, 5.76}
ENCOUNTER_DECK_DISCARD_POSITION = {-3.85, 1, 10.38}
-- optionPanel data
optionPanel = {}
-- GUID of data helper
DATA_HELPER_GUID = "708279"
-- GUIDs that will not be interactable (e.g. parts of the table)
local NOT_INTERACTABLE = {
"6161b4", -- Decoration-Map
"721ba2", -- PlayArea
"9f334f", -- MythosArea
"463022", -- Panel behind tentacle stand
"f182ee", -- InvestigatorCount
"7bff34", -- Tentacle stand
"8646eb", -- horizontal border left
"75937e", -- horizontal border right
"612072", -- vertical border left
"975c39", -- vertical border right
}
local chaosTokens = {}
local chaosTokensLastMat = nil
local IS_RESHUFFLING = false
local bagSearchers = {}
local MAT_COLORS = {"White", "Orange", "Green", "Red"}
local hideTitleSplashWaitFunctionId = nil
local playmatApi = require("playermat/PlaymatApi")
local tokenManager = require("core/token/TokenManager")
local playAreaAPI = require("core/PlayAreaApi")
local soundCubeApi = require("core/SoundCubeApi")
local mythosAreaApi = require("core/MythosAreaApi")
local tokenArrangerApi = require("accessories/TokenArrangerApi")
local blessCurseManagerApi = require("chaosbag/BlessCurseManagerApi")
-- online functionality related variables
local MOD_VERSION = "3.1.0"
local SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'
local library, requestObj, modMeta, notificationVisible
local acknowledgedUpgradeVersions = {}
---------------------------------------------------------
-- data for tokens
---------------------------------------------------------
TOKEN_DATA = {
damage = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/", scale = {0.17, 0.17, 0.17}},
horror = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/", scale = {0.17, 0.17, 0.17}},
resource = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/", scale = {0.17, 0.17, 0.17}},
doom = {image = "https://i.imgur.com/EoL7yaZ.png", scale = {0.17, 0.17, 0.17}},
clue = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/", scale = {0.15, 0.15, 0.15}}
}
ID_URL_MAP = {
['blue'] = {name = "Elder Sign", url = 'https://i.imgur.com/nEmqjmj.png'},
['p1'] = {name = "+1", url = 'https://i.imgur.com/uIx8jbY.png'},
['0'] = {name = "0", url = 'https://i.imgur.com/btEtVfd.png'},
['m1'] = {name = "-1", url = 'https://i.imgur.com/w3XbrCC.png'},
['m2'] = {name = "-2", url = 'https://i.imgur.com/bfTg2hb.png'},
['m3'] = {name = "-3", url = 'https://i.imgur.com/yfs8gHq.png'},
['m4'] = {name = "-4", url = 'https://i.imgur.com/qrgGQRD.png'},
['m5'] = {name = "-5", url = 'https://i.imgur.com/3Ym1IeG.png'},
['m6'] = {name = "-6", url = 'https://i.imgur.com/c9qdSzS.png'},
['m7'] = {name = "-7", url = 'https://i.imgur.com/4WRD42n.png'},
['m8'] = {name = "-8", url = 'https://i.imgur.com/9t3rPTQ.png'},
['skull'] = {name = "Skull", url = 'https://i.imgur.com/stbBxtx.png'},
['cultist'] = {name = "Cultist", url = 'https://i.imgur.com/VzhJJaH.png'},
['tablet'] = {name = "Tablet", url = 'https://i.imgur.com/1plY463.png'},
['elder'] = {name = "Elder Thing", url = 'https://i.imgur.com/ttnspKt.png'},
['red'] = {name = "Auto-fail", url = 'https://i.imgur.com/lns4fhz.png'},
['bless'] = {name = "Bless", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/'},
['curse'] = {name = "Curse", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/'},
['frost'] = {name = "Frost", url = 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/'}
}
---------------------------------------------------------
-- data for chaos token stat tracker
---------------------------------------------------------
local MAT_GUID_TO_COLOR = {
["Overall"] = "Overall",
["8b081b"] = "White",
["bd0ff4"] = "Orange",
["383d8b"] = "Green",
["0840d5"] = "Red"
}
local tokenDrawingStats = {
["Overall"] = {},
["8b081b"] = {},
["bd0ff4"] = {},
["383d8b"] = {},
["0840d5"] = {}
}
---------------------------------------------------------
-- general code
---------------------------------------------------------
-- saving state of optionPanel to restore later
function onSave() return JSON.encode({ optionPanel = optionPanel, acknowledgedUpgradeVersions = acknowledgedUpgradeVersions }) end
function onLoad(savedData)
if savedData then
loadedData = JSON.decode(savedData)
optionPanel = loadedData.optionPanel
acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions
updateOptionPanelState()
-- hack to disable on load
playmatApi.clickableClues(false, "All")
2023-04-22 16:56:01 -04:00
else
print("Saved state could not be found!")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
for _, guid in ipairs(NOT_INTERACTABLE) do
local obj = getObjectFromGUID(guid)
if obj ~= nil then obj.interactable = false end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
getModVersion()
math.randomseed(os.time())
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
-- encounter card drawing
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function isDeck(x) return x.tag == 'Deck' end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function isCardOrDeck(x) return x.tag == 'Card' or x.tag == 'Deck' end
-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag
-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the
-- chaos bag during search operations to avoid this.
function onObjectSearchStart(object, playerColor)
chaosbag = findChaosBag()
if object == chaosbag then
bagSearchers[playerColor] = true
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag
-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the
-- chaos bag during search operations to avoid this.
function onObjectSearchEnd(object, playerColor)
chaosbag = findChaosBag()
if object == chaosbag then
bagSearchers[playerColor] = nil
end
end
-- Pass object enter container events to the PlayArea to clear vector lines from dragged cards.
-- This requires the try method as cards won't exist any more after they enter a deck, so the lines
-- can't be cleared.
function tryObjectEnterContainer(container, object)
playAreaAPI.tryObjectEnterContainer(container, object)
return true
end
function drawEncountercard(color) --[[ Parameter Player color ]]
2023-04-22 16:56:01 -04:00
local items = findInRadiusBy(ENCOUNTER_DECK_POS, 4, isCardOrDeck)
if #items > 0 then
for _, v in ipairs(items) do
if v.tag == 'Deck' then
v.deal(1, color)
return
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- we didn't find the deck so just pull the first thing we did find
items[1].deal(1, color)
2023-04-22 16:56:01 -04:00
else
-- nothing here, time to reshuffle
reshuffleEncounterDeck(color)
2023-04-22 16:56:01 -04:00
end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function actualEncounterCardDraw(card, params)
local position = params[1]
local rotation = params[2]
local alwaysFaceUp = params[3]
local faceUpRotation = 0
if not alwaysFaceUp then
local metadata = JSON.decode(card.getGMNotes()) or {}
if metadata.hidden or getObjectFromGUID(DATA_HELPER_GUID).call('checkHiddenCard', card.getName()) then
faceUpRotation = 180
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
card.setPositionSmooth(position, false, false)
card.setRotationSmooth({0, rotation.y, faceUpRotation}, false, false)
end
function reshuffleEncounterDeck(color)
2023-04-22 16:56:01 -04:00
-- finishes moving the deck back and draws a card
local function move(deck)
deck.setPositionSmooth({ENCOUNTER_DECK_POS[1], ENCOUNTER_DECK_POS[2] + 2, ENCOUNTER_DECK_POS[3]}, false, true)
deck.deal(1, color)
2023-04-22 16:56:01 -04:00
Wait.time(function() IS_RESHUFFLING = false end, 1)
end
-- bail out if we're mid reshuffle
if IS_RESHUFFLING then return end
local discarded = findInRadiusBy(ENCOUNTER_DECK_DISCARD_POSITION, 4, isDeck)
if #discarded > 0 then
IS_RESHUFFLING = true
local deck = discarded[1]
if not deck.is_face_down then deck.flip() end
deck.shuffle()
Wait.time(|| move(deck), 0.3)
else
printToAll("Couldn't find encounter discard pile to reshuffle.", {1, 0, 0})
end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function findInRadiusBy(pos, radius, filter)
local objList = Physics.cast({
origin = pos,
direction = {0, 1, 0},
type = 2,
size = {radius, radius, radius},
max_distance = 0
})
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local filteredList = {}
for _, obj in ipairs(objList) do
if filter and filter(obj.hit_object) then
table.insert(filteredList, obj.hit_object)
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
return filteredList
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
-- chaos token drawing
---------------------------------------------------------
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- checks scripting zone for chaos bag (also called by a lot of objects!)
function findChaosBag()
local chaosbag_zone = getObjectFromGUID("83ef06")
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- error handling: scripting zone not found
if chaosbag_zone == nil then
printToAll("Zone for chaos bag detection couldn't be found.", "Red")
return
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
for _, item in ipairs(chaosbag_zone.getObjects()) do
if item.getDescription() == "Chaos Bag" then
return item
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- error handling: chaos bag not found
printToAll("Chaos bag couldn't be found.", "Red")
end
function returnChaosTokens()
for _, token in pairs(chaosTokens) do
if token ~= nil then chaosbag.putObject(token) end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
chaosTokens = {}
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens
-- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the
-- contents of the bag should check this method before doing so.
-- This method will broadcast a message to all players if the bag is being searched.
---@return Boolean. True if the bag is manipulated, false if it should be blocked.
function canTouchChaosTokens()
for color, searching in pairs(bagSearchers) do
if searching then
broadcastToAll("Someone is searching the chaos bag, can't touch the tokens.", "Red")
return false
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
return true
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- called by playermats (by the "Draw chaos token" button)
function drawChaosToken(params)
if not canTouchChaosTokens() then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local mat = params[1]
local tokenOffset = params[2]
local isRightClick = params[3]
chaosbag = findChaosBag()
-- return token(s) on other playmat first
if chaosTokensLastMat ~= nil and chaosTokensLastMat ~= mat and #chaosTokens ~= 0 then
returnChaosTokens()
chaosTokensLastMat = nil
return
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
chaosTokensLastMat = mat
-- if we have left clicked and have no tokens OR if we have right clicked
if isRightClick or #chaosTokens == 0 then
if #chaosbag.getObjects() == 0 then return end
chaosbag.shuffle()
-- add the token to the list, compute new position based on list length
tokenOffset[1] = tokenOffset[1] + (0.17 * #chaosTokens)
local token = chaosbag.takeObject({
index = 0,
position = mat.positionToWorld(tokenOffset),
rotation = mat.getRotation()
})
-- get data for token description
local name = token.getName()
local tokenData = mythosAreaApi.returnTokenData().tokenData or {}
local specificData = tokenData[name] or {}
token.setDescription(specificData.description or "")
-- track the chaos token (for stat tracker and future returning)
trackChaosToken(name, mat.getGUID())
chaosTokens[#chaosTokens + 1] = token
return
else
returnChaosTokens()
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
-- token spawning
---------------------------------------------------------
-- DEPRECATED. Use TokenManager instead.
-- Spawns a single token.
---@param params Table. Array with arguments to the method. 1 = position, 2 = type, 3 = rotation
function spawnToken(params)
return tokenManager.spawnToken(params[1], params[2], params[3])
end
---------------------------------------------------------
-- chaos token stat tracker
---------------------------------------------------------
function trackChaosToken(tokenName, matGUID)
tokenDrawingStats["Overall"][tokenName] = (tokenDrawingStats["Overall"][tokenName] or 0) + 1
tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + 1
end
-- Left-click: print stats, Right-click: reset stats
function handleStatTrackerClick(_, _, isRightClick)
if isRightClick then
for key, _ in pairs(tokenDrawingStats) do
tokenDrawingStats[key] = {}
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
else
local squidKing = "Nobody"
local maxSquid = 0
local playerColor, playerName
for key, personalStats in pairs(tokenDrawingStats) do
if personalStats ~= {} then
if key == "Overall" then
playerColor = "White"
playerName = "Overall"
else
playerColor = playmatApi.getPlayerColor(MAT_GUID_TO_COLOR[key])
playerName = Player[playerColor].steam_name or playerColor
local playerSquidCount = personalStats["Auto-fail"] or 0
if playerSquidCount > maxSquid then
squidKing = playerName
maxSquid = playerSquidCount
end
end
printToAll("------------------------------")
printToAll(playerName .. " Stats", playerColor)
for tokenName, value in pairs(personalStats) do
if value then
printToAll(tokenName .. ': ' .. tostring(value))
end
2023-01-29 19:31:52 -05:00
end
end
end
2023-04-22 16:56:01 -04:00
printToAll("------------------------------")
printToAll(squidKing .. " is an auto-fail magnet.", {255, 0, 0})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
-- Difficulty selector script
---------------------------------------------------------
-- called for button creation on the difficulty selectors
---@param object object Usually "self"
---@param key string Name of the scenario
function createSetupButtons(args)
local data = getDataValue('modeData', args.key)
if data ~= nil then
local buttonParameters = {}
buttonParameters.function_owner = args.object
buttonParameters.position = {0, 0.1, -0.15}
buttonParameters.scale = {0.47, 1, 0.47}
buttonParameters.height = 200
buttonParameters.width = 1150
buttonParameters.color = {0.87, 0.8, 0.7}
if data.easy ~= nil then
buttonParameters.label = "Easy"
buttonParameters.click_function = "easyClick"
args.object.createButton(buttonParameters)
buttonParameters.position[3] = buttonParameters.position[3] + 0.20
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
if data.normal ~= nil then
buttonParameters.label = "Standard"
buttonParameters.click_function = "normalClick"
args.object.createButton(buttonParameters)
buttonParameters.position[3] = buttonParameters.position[3] + 0.20
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
if data.hard ~= nil then
buttonParameters.label = "Hard"
buttonParameters.click_function = "hardClick"
args.object.createButton(buttonParameters)
buttonParameters.position[3] = buttonParameters.position[3] + 0.20
end
if data.expert ~= nil then
buttonParameters.label = "Expert"
buttonParameters.click_function = "expertClick"
args.object.createButton(buttonParameters)
buttonParameters.position[3] = buttonParameters.position[3] + 0.20
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
if data.standalone ~= nil then
buttonParameters.label = "Standalone"
buttonParameters.click_function = "standaloneClick"
args.object.createButton(buttonParameters)
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- called for adding chaos tokens
---@param object object Usually "self"
---@param key string Name of the scenario
---@param mode string difficulty (e.g. "hard" or "expert")
function fillContainer(args)
local data = getDataValue('modeData', args.key)
if data == nil then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local value = data[args.mode]
if value == nil or value.token == nil then return end
local tokenList = {}
for _, tokenId in ipairs(value.token) do
table.insert(tokenList, tokenId)
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
if value.append ~= nil then
for _, tokenId in ipairs(value.append) do
table.insert(tokenList, tokenId)
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- randomly choose tokens for specific Carcosa scenarios in standalone
if value.random then
local n = #value.random
if n > 0 then
for _, tokenId in ipairs(value.random[math.random(1, n)]) do
table.insert(tokenList, tokenId)
2023-01-29 19:31:52 -05:00
end
end
end
2023-04-22 16:56:01 -04:00
setChaosBagState(tokenList)
if value.message then
broadcastToAll(value.message)
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
if value.warning then
broadcastToAll(value.warning, { 1, 0.5, 0.5 })
end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function getDataValue(storage, key)
local data = getObjectFromGUID(DATA_HELPER_GUID).getTable(storage)
if data ~= nil then
local value = data[key]
if value ~= nil then
local res = {}
for m, v in pairs(value) do
res[m] = v
if res[m].parent ~= nil then
local parentData = getDataValue(storage, res[m].parent)
if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then
res[m].token = parentData[m].token
end
res[m].parent = nil
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return res
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- respawns the chaos bag with a new state of tokens
---@param tokenList Table List of chaos token ids
function setChaosBagState(tokenList)
if not canTouchChaosTokens() then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local chaosbag = findChaosBag()
local chaosbagData = chaosbag.getData()
local reserveData = getObjectFromGUID("106418").getData()
local tokenCache = {}
local containedObjects = {}
-- create a temporary copy of the data for each chaos token
for _, objData in ipairs(reserveData.ContainedObjects) do
tokenCache[objData.Nickname] = objData
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- iterate over tokenlist and insert specified tokens into new table
for _, tokenId in ipairs(tokenList) do
local tokenName = ID_URL_MAP[tokenId].name
table.insert(containedObjects, tokenCache[tokenName])
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- overwrite chaos bag content and respawn it
chaosbagData.ContainedObjects = containedObjects
chaosbag.destruct()
spawnObjectData({data = chaosbagData})
-- remove tokens that are still in play
for _, token in pairs(chaosTokens) do
if token ~= nil then token.destruct() end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
chaosTokens = {}
chaosTokensLastMat = nil
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- reset bless / curse manager
blessCurseManagerApi.removeTakenTokensAndReset()
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
printToAll("Chaos bag set to chosen difficulty.", "Green")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- spawns the specified chaos token and puts it into the chaos bag
---@param id String ID of the chaos token
function spawnChaosToken(id)
if not canTouchChaosTokens() then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
id = id:lower()
local chaosbag = findChaosBag()
local url = ID_URL_MAP[id].url or ""
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
if url ~= "" then
return spawnObject({
type = 'Custom_Tile',
position = { 0.49, 3, 0 },
scale = { 0.81, 1.0, 0.81 },
rotation = {0, 270, 0},
callback_function = function(obj)
obj.setName(ID_URL_MAP[id].name)
chaosbag.putObject(obj)
tokenArrangerApi.layout()
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
}).setCustomObject({
type = 2,
image = url,
thickness = 0.1
})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- removes the specified chaos token from the chaos bag
---@param id String ID of the chaos token
function removeChaosToken(id)
if not canTouchChaosTokens() then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local tokens = {}
local chaosbag = findChaosBag()
local name = ID_URL_MAP[id].name
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
for _, v in ipairs(chaosbag.getObjects()) do
if v.name == name then table.insert(tokens, v.guid) end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- error handling: no matching token found
if #tokens == 0 then
printToAll("No " .. name .. " tokens in the chaos bag.", "Yellow")
return
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
chaosbag.takeObject({
guid = tokens[1],
smooth = false,
callback_function = function(obj)
obj.destruct()
tokenArrangerApi.layout()
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
})
printToAll("Removing " .. name .. " token (in bag: " .. #tokens - 1 .. ")", "White")
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- empty the chaos bag
function emptyChaosBag()
if not canTouchChaosTokens() then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local chaosbag = findChaosBag()
for _, object in ipairs(chaosbag.getObjects()) do
chaosbag.takeObject({callback_function = function(item) item.destruct() end})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
---------------------------------------------------------
-- Content Importing and XML functions
---------------------------------------------------------
function onClick_refreshList()
local request = WebRequest.get(SOURCE_REPO .. '/library.json', completed_list_update)
requestObj = request
startLuaCoroutine(Global, 'downloadCoroutine')
end
function onClick_select(player, params)
params = JSON.decode(urldecode(params))
local url = SOURCE_REPO .. '/' .. params.url
local request = WebRequest.get(url, function (request) complete_obj_download(request, params) end )
requestObj = request
startLuaCoroutine(Global, 'downloadCoroutine')
end
function onClick_load()
UI.show('progress_display')
UI.hide('load_button')
end
function onClick_toggleUi(_, title)
UI.hide('optionPanel')
UI.hide('load_ui')
-- when same button is clicked or close window button is pressed, don't open UI
if UI.getValue('title') ~= title and title ~= 'Hidden' then
UI.setValue('title', title)
if title == "Options" then
UI.show('optionPanel')
else
update_window_content(title)
UI.show('load_ui')
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
else
UI.setValue('title', "Hidden")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function downloadCoroutine()
while requestObj do
UI.setAttribute('download_progress', 'percentage', requestObj.download_progress * 100)
coroutine.yield(0)
end
return 1
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
function update_list(objects)
local ui = UI.getXmlTable()
local update_height = find_tag_with_id(ui, 'ui_update_height')
local update_children = find_tag_with_id(update_height.children, 'ui_update_point')
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
update_children.children = {}
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
for _, v in ipairs(objects) do
local s = JSON.encode(v);
table.insert(update_children.children,
{ tag = 'Text',
value = v.name,
attributes = { onClick = 'onClick_select(' .. urlencode(JSON.encode(v)) .. ')', alignment = 'MiddleLeft' }
})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
update_height.attributes.height = #(update_children.children) * 24
UI.setXmlTable(ui)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function update_window_content(new_title)
if not library then return end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
if new_title == 'Campaigns' then
update_list(library.campaigns)
elseif new_title == 'Standalone Scenarios' then
update_list(library.scenarios)
elseif new_title == 'Investigators' then
update_list(library.investigators)
elseif new_title == 'Community Content' then
update_list(library.community)
elseif new_title == 'Extras' then
update_list(library.extras)
else
update_list({})
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function complete_obj_download(request, params)
assert(request.is_done)
if request.is_error or request.response_code ~= 200 then
print('error: ' .. request.error)
else
if pcall(function()
local replaced_object
pcall(function()
if params.replace then
replaced_object = getObjectFromGUID(params.replace)
end
end)
local json = request.text
if replaced_object then
local pos = replaced_object.getPosition()
local rot = replaced_object.getRotation()
destroyObject(replaced_object)
Wait.frames(function()
spawnObjectJSON({json = json, position = pos, rotation = rot})
end, 1)
else
spawnObjectJSON({json = json})
end
end) then
print('Object loaded.')
else
print('Error loading object.')
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
requestObj = nil
UI.setAttribute('download_progress', 'percentage', 100)
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- the download button on the placeholder objects calls this to directly initiate a download
-- params is a table with url and guid of replacement object, which happens to match what onClick_select wants
function placeholder_download(params)
onClick_select(nil, JSON.encode(params))
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
function completed_list_update(request)
assert(request.is_done)
if request.is_error or request.response_code ~= 200 then
print('error: ' .. request.error)
else
local json_response = nil
if pcall(function () json_response = JSON.decode(request.text) end) then
library = json_response
update_window_content(UI.getValue('title'))
else
print('error parsing downloaded library')
end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
requestObj = nil
UI.setAttribute('download_progress', 'percentage', 100)
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
function find_tag_with_id(ui, id)
for _, obj in ipairs(ui) do
if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end
if obj.children then
local result = find_tag_with_id(obj.children, id)
if result then return result end
end
end
return nil
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
function urlencode(str)
local str = string.gsub(str, "([^A-Za-z0-9-_.~])",
function (c) return string.format("%%%02X", string.byte(c)) end)
return str
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
function urldecode(str)
local str = string.gsub(str, "%%(%x%x)",
function (h) return string.char(tonumber(h, 16)) end)
return str
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
-- Option Panel related functionality
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
-- called by toggling an option
function onClick_toggleOption(_, id)
local state = self.UI.getAttribute(id, "isOn")
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- flip state (and handle stupid "False" value)
if state == "False" then
state = true
else
state = false
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
self.UI.setAttribute(id, "isOn", state)
applyOptionPanelChange(id, state)
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- sets the option panel to the correct state (corresponding to 'optionPanel')
function updateOptionPanelState()
for id, enabled in pairs(optionPanel) do
if (type(enabled) == "boolean" and enabled)
or (type(enabled) == "string" and enabled)
or (type(enabled) == "table" and #enabled ~= 0) then
self.UI.setAttribute(id, "isOn", true)
else
self.UI.setAttribute(id, "isOn", "False")
end
end
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- handles the applying of option selections and calls the respective functions based
---@param id String ID of the option that was selected or deselected
---@param state Boolean State of the option (true = enabled)
function applyOptionPanelChange(id, state)
-- option: Snap tags
if id == "useSnapTags" then
playmatApi.setLimitSnapsByType(state, "All")
optionPanel[id] = state
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Draw 1 button
elseif id == "showDrawButton" then
playmatApi.showDrawButton(state, "All")
optionPanel[id] = state
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Clickable clue counters
elseif id == "useClueClickers" then
playmatApi.clickableClues(state, "All")
optionPanel[id] = state
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- update master clue counter
getObjectFromGUID("4a3aa4").setVar("useClickableCounters", state)
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Clickable resource counters
elseif id == "useResourceCounters" then
optionPanel[id] = state
2020-12-06 09:42:32 -05:00
2023-04-22 16:56:01 -04:00
-- option: Play area snap tags
elseif id == "playAreaSnapTags" then
playAreaAPI.setLimitSnapsByType(state)
optionPanel[id] = state
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show Title on placing scenarios
elseif id == "showTitleSplash" then
optionPanel[id] = state
-- option: Show token arranger
elseif id == "showTokenArranger" then
optionPanel[id] = spawnOrRemoveHelper(state, "Token Arranger", {-42.3, 1.6, -46.5})
-- option: Show clean up helper
elseif id == "showCleanUpHelper" then
optionPanel[id] = spawnOrRemoveHelper(state, "Clean Up Helper", {-66, 1.6, 46})
-- option: Show hand helper for each player
elseif id == "showHandHelper" then
for i, color in ipairs(MAT_COLORS) do
local pos = playmatApi.transformLocalPosition({0.05, 0, -1.182}, color)
local rot = playmatApi.returnRotation(color)
optionPanel[id][i] = spawnOrRemoveHelper(state, "Hand Helper", pos, rot)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show search assistant for each player
elseif id == "showSearchAssistant" then
for i, color in ipairs(MAT_COLORS) do
local pos = playmatApi.transformLocalPosition({-0.3, 0, -1.182}, color)
local rot = playmatApi.returnRotation(color)
optionPanel[id][i] = spawnOrRemoveHelper(state, "Search Assistant", pos, rot)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show chaos bag manager
elseif id == "showChaosBagManager" then
optionPanel[id] = spawnOrRemoveHelper(state, "Chaos Bag Manager", {-66, 1.6, -49.5})
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show attachment helper
elseif id == "showAttachmentHelper" then
optionPanel[id] = spawnOrRemoveHelper(state, "Attachment Helper", {-62, 1.4, 0})
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show navigation overlay
elseif id == "showNavigationOverlay" then
optionPanel[id] = spawnOrRemoveHelper(state, "jaqenZann's Navigation Overlay", {-68, 1.4, -70})
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show CYOA campaign guides
elseif id == "showCYOA" then
optionPanel[id] = spawnOrRemoveHelper(state, "CYOA Campaign Guides", {65, 1.6, -11})
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show custom playmat images
elseif id == "showCustomPlaymatImages" then
optionPanel[id] = spawnOrRemoveHelper(state, "Custom Playmat Images", {67.5, 1.6, 37})
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- option: Show displacement tool
elseif id == "showDisplacementTool" then
optionPanel[id] = spawnOrRemoveHelper(state, "Displacement Tool", {-57, 1.6, 46})
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- handler for spawn / remove functions of helper objects
---@param state Boolean Contains the state of the option: true = spawn it, false = remove it
---@param name String Name of the helper object
---@param position Vector Position of the object (where it will spawn)
---@param rotation Vector Rotation of the object for spawning (default: {0, 270, 0})
---@return. GUID of the spawnedObj (or nil if object was removed)
function spawnOrRemoveHelper(state, name, position, rotation)
if state then
Player.getPlayers()[1].pingTable(position)
return spawnHelperObject(name, position, rotation).getGUID()
else
return removeHelperObject(name)
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- copies the specified tool (by name) from the option panel source bag
---@param name String Name of the object that should be copied
---@param position Table Desired position of the object
function spawnHelperObject(name, position, rotation)
local sourceBag = getObjectFromGUID("830bd0")
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- error handling for missing sourceBag
if not sourceBag then
broadcastToAll("Option panel source bag could not be found!", "Red")
return
2020-11-28 13:23:58 -05:00
end
2021-02-13 12:12:29 -05:00
2023-04-22 16:56:01 -04:00
local spawnTable = {position = position}
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- only overrride rotation if there is one provided (object's rotation used instead)
if rotation then
spawnTable.rotation = rotation
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
for _, obj in ipairs(sourceBag.getData().ContainedObjects) do
if obj["Nickname"] == name then
spawnTable.data = obj
spawnTable.callback_function = function(spawnedObj)
Wait.time(function() spawnedObj.setLock(true) end, 2)
end
return spawnObjectData(spawnTable)
2020-11-28 13:23:58 -05:00
end
end
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
-- removes the specified tool (by name)
---@param name String Object that should be removed
function removeHelperObject(name)
-- links objects name to the respective option name (to grab the GUID for removal)
local referenceTable = {
["Token Arranger"] = "showTokenArranger",
["Clean Up Helper"] = "showCleanUpHelper",
["Hand Helper"] = "showHandHelper",
["Search Assistant"] = "showSearchAssistant",
["Chaos Bag Manager"] = "showChaosBagManager",
["jaqenZann's Navigation Overlay"] = "showNavigationOverlay",
["Displacement Tool"] = "showDisplacementTool",
["Custom Playmat Images"] = "showCustomPlaymatImages",
["Attachment Helper"] = "showAttachmentHelper",
["CYOA Campaign Guides"] = "showCYOA"
}
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
local data = optionPanel[referenceTable[name]]
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- if there is a GUID stored, remove that object
if type(data) == "string" then
local obj = getObjectFromGUID(data)
if obj then obj.destruct() end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- if it is a table (e.g. for the "Hand Helper", remove all of them)
elseif type(data) == "table" then
for _, guid in pairs(data) do
local obj = getObjectFromGUID(guid)
if obj then obj.destruct() end
2023-01-29 19:31:52 -05:00
end
end
end
2023-04-22 16:56:01 -04:00
-- loads the default options
function onClick_defaultSettings()
for id, _ in pairs(optionPanel) do
local state = false
-- override for settings that are enabled by default
if id == "useSnapTags" or id == "showTitleSplash" then
state = true
end
applyOptionPanelChange(id, state)
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- clean reset of variable
optionPanel = {
playAreaSnapTags = true,
showAttachmentHelper = false,
showCleanUpHelper = false,
showChaosBagManager = false,
showCustomPlaymatImages = false,
showCYOA = false,
showDisplacementTool = false,
showDrawButton = false,
showHandHelper = {},
showNavigationOverlay = false,
showSearchAssistant = {},
showTitleSplash = true,
showTokenArranger = false,
useClueClickers = false,
useSnapTags = true
}
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- update UI
updateOptionPanelState()
end
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- splash scenario title on setup
function titleSplash(scenarioName)
if optionPanel['showTitleSplash'] then
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- if there's any ongoing title being displayed, hide it and cancel the waiting function
if hideTitleSplashWaitFunctionId then
Wait.stop(hideTitleSplashWaitFunctionId)
hideTitleSplashWaitFunctionId = nil
UI.setAttribute('title_splash', 'active', false)
end
-- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)
-- wait timer to hide the scenario name
UI.setValue('title_splash_text', scenarioName)
UI.show('title_splash')
hideTitleSplashWaitFunctionId = Wait.time(function()
UI.hide('title_splash')
hideTitleSplashWaitFunctionId = nil
end, 4)
soundCubeApi.playSoundByName("Deep Bell")
2020-11-28 13:23:58 -05:00
end
end
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
-- Update notification related functionality
2022-12-13 14:02:30 -05:00
---------------------------------------------------------
2023-04-22 16:56:01 -04:00
-- grabs the latest mod version and release notes from GitHub (called onLoad())
function getModVersion()
WebRequest.get(SOURCE_REPO .. '/modversion.json', compareVersion)
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
-- compares the modversion with GitHub and possibly shows the update notification
function compareVersion(request)
if request.is_error then
log(request.error)
return
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- global variable to make it accessible for other functions
modMeta = JSON.decode(request.text)
-- stop here if on latest version
if MOD_VERSION == modMeta["latestVersion"] then return end
-- stop here if "don't show again" was clicked for this version before
if acknowledgedUpgradeVersions[modMeta["latestVersion"]] then return end
updateNotificationLoading()
-- delay to avoid lagging during onLoad()
Wait.time(function() UI.show("FinnIcon") end, 1)
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
-- updates the XML update notification based on the mod metadata
function updateNotificationLoading()
-- grab data
local highlights = modMeta["releaseHighlights"]
-- concatenate the release highlights
local highlightText = "• " .. highlights[1]
for i, entry in pairs(highlights) do
if i ~= 1 then
highlightText = highlightText .. "\n• " .. entry
end
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
-- update the XML UI
UI.setValue("notificationHeader", "New version available: ".. modMeta["latestVersion"])
UI.setValue("releaseHighlightText", highlightText)
UI.setAttribute("highlightRow", "preferredHeight", 20*#highlights)
UI.setAttribute("updateNotification", "height", 20*#highlights + 125)
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
-- triggered by clicking on the Finn Icon
function onClick_FinnIcon()
if notificationVisible then
UI.hide("updateNotification")
notificationVisible = false
else
UI.show("updateNotification")
notificationVisible = true
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
end
-- close / don't show again buttons on the update notification
function onClick_notification(_, parameter)
if parameter == "dontShowAgain" then
-- this variable tracks if "don't show again" was pressed for a version
acknowledgedUpgradeVersions[modMeta["latestVersion"]] = true
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
UI.hide("FinnIcon")
UI.hide("updateNotification")
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
end)
__bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local TokenArrangerApi = {}
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- local function to call the token arranger, if it is on the table
---@param functionName String Name of the function to cal
---@param argument Variant Parameter to pass
local function callIfExistent(functionName, argument)
local tokenArranger = getObjectsWithTag("TokenArranger")[1]
if tokenArranger ~= nil then
tokenArranger.call(functionName, argument)
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- updates the token modifiers with the provided data
---@param tokenData Table Contains the chaos token metadata
TokenArrangerApi.onTokenDataChanged = function(fullData)
callIfExistent("onTokenDataChanged", fullData)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- deletes already laid out tokens
TokenArrangerApi.deleteCopiedTokens = function()
callIfExistent("deleteCopiedTokens")
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- updates the laid out tokens
TokenArrangerApi.layout = function()
Wait.time(function() callIfExistent("layout") end, 0.1)
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
return TokenArrangerApi
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
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")
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
local PLAYER_CARD_TOKEN_OFFSETS = {
[1] = {
Vector(0, 3, -0.2)
},
[2] = {
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[3] = {
Vector(0, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[4] = {
Vector(0.4, 3, -0.9),
Vector(-0.4, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[5] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.4, 3, -0.2),
Vector(-0.4, 3, -0.2)
},
[6] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2)
},
[7] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0, 3, 0.5)
},
[8] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(-0.35, 3, 0.5),
Vector(0.35, 3, 0.5)
},
[9] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5)
},
[10] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0, 3, 1.2)
},
[11] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(-0.35, 3, 1.2),
Vector(0.35, 3, 1.2)
},
[12] = {
Vector(0.7, 3, -0.9),
Vector(0, 3, -0.9),
Vector(-0.7, 3, -0.9),
Vector(0.7, 3, -0.2),
Vector(0, 3, -0.2),
Vector(-0.7, 3, -0.2),
Vector(0.7, 3, 0.5),
Vector(0, 3, 0.5),
Vector(-0.7, 3, 0.5),
Vector(0.7, 3, 1.2),
Vector(0, 3, 1.2),
Vector(-0.7, 3, 1.2)
}
}
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- Source for tokens
local TOKEN_SOURCE_GUID = "124381"
2022-12-13 14:02:30 -05:00
2023-04-22 16:56:01 -04:00
-- Table of data extracted from the token source bag, keyed by the Memo on each token which
-- should match the token type keys ("resource", "clue", etc)
local tokenTemplates
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
local DATA_HELPER_GUID = "708279"
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
local playerCardData
local locationData
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
local TokenManager = { }
local internal = { }
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- Spawns tokens for the card. This function is built to just throw a card at it and let it do
-- the work once a card has hit an area where it might spawn tokens. It will check to see if
-- the card has already spawned, find appropriate data from either the uses metadata or the Data
-- Helper, and spawn the tokens.
---@param card 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)
2020-11-28 13:23:58 -05:00
end
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- 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
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- 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
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
local rot = card.getRotation()
TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end)
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
TokenManager.spawnResourceCounterToken = function(card, tokenCount)
local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
local rot = card.getRotation()
TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
spawned.call("updateVal", tokenCount)
end)
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- Spawns a number of tokens.
---@param tokenType String type of token to spawn, valid values are resource", "doom", or "clue".
-- Other types should use spawnCounterToken()
---@param tokenCount Number How many tokens to spawn
---@param shiftDown Number An offset for the z-value of this group of tokens
TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown)
if tokenCount < 1 or tokenCount > 12 then
return
2020-12-06 09:42:32 -05:00
end
2023-04-22 16:56:01 -04:00
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
2022-12-13 14:02:30 -05:00
end
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
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
2022-12-13 14:02:30 -05:00
end
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
return
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
for i = 1, tokenCount do
TokenManager.spawnToken(offsets[i], tokenType, card.getRotation())
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
end
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04: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"
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
if tokenTemplates[loadTokenType] == nil then
error("Unknown token type '" .. tokenType .. "'")
return
end
local tokenTemplate = tokenTemplates[loadTokenType]
2020-11-28 13:23:58 -05:00
2023-04-22 16:56:01 -04:00
-- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
local rot = Vector(tokenTemplate.Transform.rotX,
270,
tokenTemplate.Transform.rotZ)
if rotation ~= nil then
rot.y = rotation.y
end
if tokenType == "doom" then
rot.z = 180
2020-11-28 13:23:58 -05:00
end
2023-04-22 16:56:01 -04:00
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
rotation = rot,
callback_function = callback
2022-12-13 14:02:30 -05:00
})
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04: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
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Pushes new player card data into the local copy of the Data Helper player data.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addPlayerCardData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
playerCardData[k] = v
2023-01-29 19:31:52 -05:00
end
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
-- Pushes new location data into the local copy of the Data Helper location data.
---@param dataTable Table Key/Value pairs following the DataHelper style
TokenManager.addLocationData = function(dataTable)
internal.initDataHelperData()
for k, v in pairs(dataTable) do
locationData[k] = v
end
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
-- 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
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
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
2021-10-18 15:54:27 -04:00
2023-04-22 16:56:01 -04:00
-- 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')
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
-- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card 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]
2022-12-13 14:02:30 -05:00
end
2023-04-22 16:56:01 -04:00
log("Spawning single use tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
TokenManager.spawnTokenGroup(card, token, tokenCount)
2021-10-18 15:54:27 -04:00
else
2023-04-22 16:56:01 -04:00
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
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
tokenSpawnTracker.markTokensSpawned(card.getGUID())
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
-- Spawn tokens for a card based on the data helper data. This will consider the face up/down state
-- of the card for both locations and standard cards.
---@param card 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)
2021-10-18 15:54:27 -04:00
end
2023-04-22 16:56:01 -04:00
local locationData = internal.getLocationData(card)
if locationData ~= nil then
internal.spawnLocationTokensFromDataHelper(card, locationData)
2021-10-18 15:54:27 -04:00
end
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- 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())
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- 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())
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
internal.getPlayerCardData = function(card)
return playerCardData[card.getName() .. ':' .. card.getDescription()]
or playerCardData[card.getName()]
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
internal.getLocationData = function(card)
return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
internal.getClueCountFromData = function(card, locationData)
-- Return the number of clues to spawn on this location
if locationData == nil then
error('attempted to get clue for unexpected object: ' .. card.getName())
return 0
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
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
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- 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
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
elseif not card.is_face_down then
return metadata.uses
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return nil
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- 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
2023-07-08 12:05:36 -04:00
local column = math.floor((i - 1) / 4)
local row = (i - 1) % 4
table.insert(cluePositions, Vector(pos.x - 1 + 0.55 * row, pos.y, pos.z - 1.4 - 0.55 * column))
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return cluePositions
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return TokenManager
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
end)
__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local PlaymatApi = { }
local internal = { }
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local MAT_IDS = {
White = "8b081b",
Orange = "bd0ff4",
Green = "383d8b",
Red = "0840d5"
2023-01-29 19:31:52 -05:00
}
2023-04-22 16:56:01 -04:00
local CLUE_COUNTER_GUIDS = {
White = "37be78",
Orange = "1769ed",
Green = "032300",
Red = "d86b7c"
}
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
local CLUE_CLICKER_GUIDS = {
White = "db85d6",
Orange = "3f22e5",
Green = "891403",
Red = "4111de"
}
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Returns the color of the by position requested playermat as string
---@param startPos Table Position of the search, table get's roughly cut into 4 quarters to assign a playermat
PlaymatApi.getMatColorByPosition = function(startPos)
if startPos.x < -42 then
if startPos.z > 0 then
return "White"
else
return "Orange"
end
else
if startPos.z > 0 then
return "Green"
else
return "Red"
end
2023-01-29 19:31:52 -05:00
end
end
2023-04-22 16:56:01 -04:00
-- Returns the color of the player's hand that is seated next to the playermat
---@param matColor String Color of the playermat
PlaymatApi.getPlayerColor = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.getVar("playerColor")
end
-- Returns the color of the playermat that owns the playercolor's hand
---@param handColor String Color of the playermat
PlaymatApi.getMatColor = function(handColor)
local matColors = {"White", "Orange", "Green", "Red"}
for i, mat in ipairs(internal.getMatForColor("All")) do
local color = mat.getVar("playerColor")
if color == handColor then return matColors[i] end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return "NOT_FOUND"
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat
---@param matColor String Color of the playermat
PlaymatApi.isDES = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.getVar("isDES")
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Returns the draw deck of the requested playmat
---@param matColor String Color of the playermat
PlaymatApi.getDrawDeck = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
mat.call("getDrawDiscardDecks")
return mat.getVar("drawDeck")
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Returns the position of the discard pile of the requested playmat
---@param matColor String Color of the playermat
PlaymatApi.getDiscardPosition = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.call("returnGlobalDiscardPosition")
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Transforms a local position into a global position
---@param localPos Table Local position to be transformed
---@param matColor String Color of the playermat
PlaymatApi.transformLocalPosition = function(localPos, matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.positionToWorld(localPos)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Returns the rotation of the requested playmat
---@param matColor String Color of the playermat
PlaymatApi.returnRotation = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.getRotation()
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Triggers the Upkeep for the requested playmat
---@param matColor String Color of the playermat
---@param playerColor String Color of the calling player (for messages)
PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.call("doUpkeepFromHotkey", playerColor)
end
2023-01-29 19:31:52 -05:00
2023-04-22 16:56:01 -04:00
-- Returns the active investigator id
---@param matColor String Color of the playermat
PlaymatApi.returnInvestigatorId = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.getVar("activeInvestigatorId")
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Sets the requested 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 matchCardTypes Boolean. Whether snap points should only snap for the matching card
-- types.
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will apply the setting to all four mats.
PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("setLimitSnapsByType", matchCardTypes)
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Sets the requested playermat's draw 1 button to visible
---@param isDrawButtonVisible Boolean. Whether the draw 1 button should be visible or not
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will apply the setting to all four mats.
PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("showDrawButton", isDrawButtonVisible)
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Shows or hides the clickable clue counter for the requested playermat
---@param showCounter Boolean. Whether the clickable counter should be present or not
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will apply the setting to all four mats.
PlaymatApi.clickableClues = function(showCounter, matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("clickableClues", showCounter)
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will apply the setting to all four mats.
PlaymatApi.removeClues = function(matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("removeClues")
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Reports the clue count for the requested playermat
---@param useClickableCounters Boolean Controls which type of counter is getting checked
PlaymatApi.getClueCount = function(useClickableCounters, matColor)
local count = 0
for _, mat in ipairs(internal.getMatForColor(matColor)) do
count = count + tonumber(mat.call("getClueCount", useClickableCounters))
end
return count
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Adds the specified amount of resources to the requested playermat's resource counter
PlaymatApi.gainResources = function(amount, matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("gainResources", amount)
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Discard a non-hidden card from the corresponding player's hand
PlaymatApi.doDiscardOne = function(matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("doDiscardOne")
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
-- Convenience function to look up a mat's object by color, or get all mats.
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will return all four mats.
---@return: Array of playermat objects. If a single mat is requested, will return a single-element
-- array to simplify processing by consumers.
internal.getMatForColor = function(matColor)
local targetMatGuid = MAT_IDS[matColor]
if targetMatGuid != nil then
return { getObjectFromGUID(targetMatGuid) }
end
if matColor == "All" then
return {
getObjectFromGUID(MAT_IDS.White),
getObjectFromGUID(MAT_IDS.Orange),
getObjectFromGUID(MAT_IDS.Green),
getObjectFromGUID(MAT_IDS.Red),
}
end
2023-01-29 19:31:52 -05:00
end
2023-04-22 16:56:01 -04:00
return PlaymatApi
2023-01-29 19:31:52 -05:00
end
2022-12-13 14:02:30 -05:00
end)
2022-12-13 18:01:00 -05:00
return __bundle_require("__root")