2023-01-29 19:31:52 -05:00
|
|
|
-- Bundled by luabundle {"version":"1.6.0"}
|
|
|
|
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
|
|
|
|
local loadingPlaceholder = {[{}] = true}
|
|
|
|
|
|
|
|
local register
|
|
|
|
local modules = {}
|
|
|
|
|
|
|
|
local require
|
|
|
|
local loaded = {}
|
|
|
|
|
|
|
|
register = function(name, body)
|
|
|
|
if not modules[name] then
|
|
|
|
modules[name] = body
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
require = function(name)
|
|
|
|
local loadedModule = loaded[name]
|
|
|
|
|
|
|
|
if loadedModule then
|
|
|
|
if loadedModule == loadingPlaceholder then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if not modules[name] then
|
|
|
|
if not superRequire then
|
|
|
|
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
|
|
|
|
error('Tried to require ' .. identifier .. ', but no such module has been registered')
|
|
|
|
else
|
|
|
|
return superRequire(name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
loaded[name] = loadingPlaceholder
|
|
|
|
loadedModule = modules[name](require, loaded, register, modules)
|
|
|
|
loaded[name] = loadedModule
|
|
|
|
end
|
|
|
|
|
|
|
|
return loadedModule
|
|
|
|
end
|
|
|
|
|
|
|
|
return require, loaded, register, modules
|
|
|
|
end)(nil)
|
2024-07-27 21:47:52 -04:00
|
|
|
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
2024-01-06 21:32:29 -05:00
|
|
|
do
|
2024-07-27 21:47:52 -04:00
|
|
|
local PlayAreaApi = {}
|
2024-06-09 10:10:21 -04:00
|
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
2024-01-06 21:32:07 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local function getPlayArea()
|
|
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea")
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-01-06 21:32:07 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local function getInvestigatorCounter()
|
|
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter")
|
2024-01-06 21:32:07 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns the current value of the investigator counter from the playermat
|
|
|
|
---@return number: Number of investigators currently set on the counter
|
|
|
|
PlayAreaApi.getInvestigatorCount = function()
|
|
|
|
return getInvestigatorCounter().getVar("val")
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Updates the current value of the investigator counter from the playermat
|
|
|
|
---@param count number Number of investigators to set on the counter
|
|
|
|
PlayAreaApi.setInvestigatorCount = function(count)
|
|
|
|
getInvestigatorCounter().call("updateVal", count)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-02-04 10:51:51 -05:00
|
|
|
|
2024-07-27 21:47:52 -04: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 string Color of the player requesting the shift for messages
|
|
|
|
PlayAreaApi.shiftContentsUp = function(playerColor)
|
|
|
|
getPlayArea().call("shiftContentsUp", playerColor)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
PlayAreaApi.shiftContentsDown = function(playerColor)
|
|
|
|
getPlayArea().call("shiftContentsDown", playerColor)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-01-06 21:32:07 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
PlayAreaApi.shiftContentsLeft = function(playerColor)
|
|
|
|
getPlayArea().call("shiftContentsLeft", playerColor)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-01-06 21:32:07 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
PlayAreaApi.shiftContentsRight = function(playerColor)
|
|
|
|
getPlayArea().call("shiftContentsRight", playerColor)
|
|
|
|
end
|
|
|
|
|
|
|
|
---@param state boolean This controls whether location connections should be drawn
|
|
|
|
PlayAreaApi.setConnectionDrawState = function(state)
|
|
|
|
getPlayArea().call("setConnectionDrawState", state)
|
2024-01-06 21:32:07 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
---@param color string Connection color to be used for location connections
|
|
|
|
PlayAreaApi.setConnectionColor = function(color)
|
|
|
|
getPlayArea().call("setConnectionColor", color)
|
2024-01-06 21:32:07 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Event to be called when the current scenario has changed
|
|
|
|
---@param scenarioName string Name of the new scenario
|
|
|
|
PlayAreaApi.onScenarioChanged = function(scenarioName)
|
|
|
|
getPlayArea().call("onScenarioChanged", scenarioName)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-01-06 21:32:07 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Sets this playermat's snap points to limit snapping to locations or not.
|
|
|
|
-- If matchTypes is false, snap points will be reset to snap all cards.
|
|
|
|
---@param matchCardTypes boolean Whether snap points should only snap for the matching card types
|
|
|
|
PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
|
|
|
|
getPlayArea().call("setLimitSnapsByType", matchCardTypes)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged
|
|
|
|
-- cards before they're destroyed by entering the container
|
|
|
|
PlayAreaApi.tryObjectEnterContainer = function(container, object)
|
|
|
|
getPlayArea().call("tryObjectEnterContainer", { container = container, object = object })
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Counts the VP on locations in the play area
|
|
|
|
PlayAreaApi.countVP = function()
|
|
|
|
return getPlayArea().call("countVP")
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Highlights all locations in the play area without metadata
|
|
|
|
---@param state boolean True if highlighting should be enabled
|
|
|
|
PlayAreaApi.highlightMissingData = function(state)
|
|
|
|
return getPlayArea().call("highlightMissingData", state)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Highlights all locations in the play area with VP
|
|
|
|
---@param state boolean True if highlighting should be enabled
|
|
|
|
PlayAreaApi.highlightCountedVP = function(state)
|
|
|
|
return getPlayArea().call("countVP", state)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Checks if an object is in the play area (returns true or false)
|
|
|
|
PlayAreaApi.isInPlayArea = function(object)
|
|
|
|
return getPlayArea().call("isInPlayArea", object)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Returns the current surface of the play area
|
|
|
|
PlayAreaApi.getSurface = function()
|
|
|
|
return getPlayArea().getCustomObject().image
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Updates the surface of the play area
|
|
|
|
PlayAreaApi.updateSurface = function(url)
|
|
|
|
return getPlayArea().call("updateSurface", url)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Returns a deep copy of the currently tracked locations
|
|
|
|
PlayAreaApi.getTrackedLocations = function()
|
|
|
|
local t = {}
|
|
|
|
for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do
|
|
|
|
t[k] = v
|
|
|
|
end
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the
|
|
|
|
-- data to the local token manager instance.
|
|
|
|
---@param args table Single-value array holding the GUID of the Custom Data Helper making the call
|
|
|
|
PlayAreaApi.updateLocations = function(args)
|
|
|
|
getPlayArea().call("updateLocations", args)
|
|
|
|
end
|
|
|
|
|
|
|
|
PlayAreaApi.getCustomDataHelper = function()
|
|
|
|
return getPlayArea().getVar("customDataHelper")
|
|
|
|
end
|
|
|
|
|
|
|
|
return PlayAreaApi
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end)
|
2024-07-27 21:47:52 -04:00
|
|
|
__bundle_register("arkhamdb/ArkhamDb", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
do
|
|
|
|
local allCardsBagApi = require("playercards/AllCardsBagApi")
|
|
|
|
local playAreaApi = require("core/PlayAreaApi")
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local ArkhamDb = {}
|
|
|
|
local internal = {}
|
|
|
|
|
|
|
|
local tabooList = {}
|
|
|
|
local configuration
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local RANDOM_WEAKNESS_ID = "01000"
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
---@class Request
|
|
|
|
local Request = {}
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Sets up the ArkhamDb interface. Should be called from the parent object on load.
|
2024-06-09 10:10:21 -04:00
|
|
|
ArkhamDb.initialize = function()
|
|
|
|
configuration = internal.getConfiguration()
|
|
|
|
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
|
|
|
|
local json = JSON.decode(internal.fixUtf16String(status.text))
|
|
|
|
for _, taboo in pairs(json) do
|
|
|
|
local cards = {}
|
|
|
|
|
|
|
|
for _, card in pairs(JSON.decode(taboo.cards)) do
|
|
|
|
cards[card.code] = true
|
|
|
|
end
|
|
|
|
|
|
|
|
tabooList[taboo.id] = {
|
|
|
|
date = taboo.date_start,
|
|
|
|
cards = cards
|
|
|
|
}
|
|
|
|
end
|
|
|
|
return true, nil
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Start the deck build process for the given player color and deck ID. This
|
|
|
|
-- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
|
|
|
|
---@param playerColor string Color name of the player mat to place this deck on (e.g. "Red").
|
|
|
|
---@param deckId string ArkhamDB deck id to be loaded
|
|
|
|
---@param isPrivate boolean Whether this deck is published or private on ArkhamDB
|
|
|
|
---@param loadNewest boolean Whether the newest version of this deck should be loaded
|
|
|
|
---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck
|
|
|
|
---@param callback function Callback which will be sent the results of this load
|
|
|
|
--- Parameters to the callback will be:
|
|
|
|
--- slots table A map of card ID to count in the deck
|
|
|
|
--- investigatorCode String. ID of the investigator in this deck
|
|
|
|
--- customizations table The decoded table of customization upgrades in this deck
|
|
|
|
--- playerColor String. Color this deck is being loaded for
|
|
|
|
---@return boolean
|
|
|
|
---@return string
|
|
|
|
ArkhamDb.getDecklist = function(
|
|
|
|
playerColor,
|
|
|
|
deckId,
|
|
|
|
isPrivate,
|
|
|
|
loadNewest,
|
|
|
|
loadInvestigators,
|
|
|
|
callback)
|
|
|
|
-- Get a simple card to see if the bag indexes are complete. If not, abort
|
|
|
|
-- the deck load. The called method will handle player notification.
|
|
|
|
local checkCard = allCardsBagApi.getCardById("01001")
|
|
|
|
if (checkCard ~= nil and checkCard.data == nil) then
|
|
|
|
return false, "Indexing not complete"
|
|
|
|
end
|
|
|
|
|
|
|
|
local deckUri = {
|
|
|
|
configuration.api_uri,
|
|
|
|
isPrivate and configuration.private_deck or configuration.public_deck,
|
|
|
|
deckId
|
|
|
|
}
|
|
|
|
|
|
|
|
local deck = Request.start(deckUri, function(status)
|
|
|
|
if string.find(status.text, "<!DOCTYPE html>") then
|
2024-07-27 21:47:52 -04:00
|
|
|
internal.maybePrint("Private deck ID " .. deckId .. " is not shared.", playerColor)
|
2024-06-09 10:10:21 -04:00
|
|
|
return false, "Private deck " .. deckId .. " is not shared"
|
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
|
|
|
|
local json = JSON.decode(internal.fixUtf16String(status.text))
|
2024-06-09 10:10:21 -04:00
|
|
|
|
|
|
|
if not json then
|
2024-07-27 21:47:52 -04:00
|
|
|
internal.maybePrint("Deck ID " .. deckId .. " not found.", playerColor)
|
2024-06-09 10:10:21 -04:00
|
|
|
return false, "Deck not found!"
|
|
|
|
end
|
|
|
|
|
|
|
|
return true, json
|
|
|
|
end)
|
|
|
|
|
|
|
|
deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Logs that a card could not be loaded in the mod by printing it to the console in the given
|
|
|
|
-- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,
|
|
|
|
-- but prints the card ID if the name cannot be retrieved.
|
|
|
|
---@param cardId string ArkhamDB ID of the card that could not be found
|
|
|
|
---@param playerColor string Color of the player's deck that had the problem
|
|
|
|
ArkhamDb.logCardNotFound = function(cardId, playerColor)
|
|
|
|
local request = Request.start({
|
|
|
|
configuration.api_uri,
|
|
|
|
configuration.cards,
|
|
|
|
cardId
|
|
|
|
},
|
|
|
|
function(result)
|
|
|
|
local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))
|
|
|
|
local cardName = adbCardInfo.real_name
|
|
|
|
if (cardName ~= nil) then
|
|
|
|
if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
|
|
|
|
cardName = cardName .. " (" .. adbCardInfo.xp .. ")"
|
|
|
|
end
|
|
|
|
internal.maybePrint("Card not found: " .. cardName .. ", card ID " .. cardId, playerColor)
|
|
|
|
else
|
|
|
|
internal.maybePrint("Card not found in ArkhamDB/Index, ID " .. cardId, playerColor)
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Callback when the deck information is received from ArkhamDB. Parses the
|
|
|
|
-- response then applies standard transformations to the deck such as adding
|
|
|
|
-- random weaknesses and checking for taboos. Once the deck is processed,
|
|
|
|
-- passes to loadCards to actually spawn the defined deck.
|
|
|
|
---@param deck table ArkhamImportDeck
|
|
|
|
---@param playerColor string Color name of the player mat to place this deck on (e.g. "Red")
|
|
|
|
---@param loadNewest boolean Whether the newest version of this deck should be loaded
|
|
|
|
---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck
|
|
|
|
---@param callback function Callback which will be sent the results of this load.
|
|
|
|
--- Parameters to the callback will be:
|
|
|
|
--- slots table A map of card ID to count in the deck
|
|
|
|
--- investigatorCode String. ID of the investigator in this deck
|
|
|
|
--- bondedList A table of cardID keys to meaningless values. Card IDs in this list were
|
|
|
|
--- added from a parent bonded card.
|
|
|
|
--- customizations table The decoded table of customization upgrades in this deck
|
|
|
|
--- playerColor String. Color this deck is being loaded for
|
|
|
|
internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)
|
|
|
|
-- Load the next deck in the upgrade path if the option is enabled
|
|
|
|
if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
|
|
|
|
buildDeck(playerColor, deck.next_deck)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
internal.maybePrint(table.concat({ "Found decklist: ", deck.name }), playerColor)
|
|
|
|
|
|
|
|
-- Initialize deck slot table and perform common transformations. The order of these should not
|
|
|
|
-- be changed, as later steps may act on cards added in each. For example, a random weakness or
|
|
|
|
-- investigator may have bonded cards or taboo entries, and should be present
|
|
|
|
local slots = deck.slots
|
|
|
|
internal.maybeDrawRandomWeakness(slots, playerColor)
|
|
|
|
|
|
|
|
-- handles alternative investigators (parallel, promo or revised art)
|
|
|
|
local loadAltInvestigator = "normal"
|
|
|
|
if loadInvestigators then
|
|
|
|
loadAltInvestigator = internal.addInvestigatorCards(deck, slots)
|
|
|
|
end
|
|
|
|
|
|
|
|
internal.maybeModifyDeckFromDescription(slots, deck.description_md, playerColor)
|
|
|
|
internal.maybeAddSummonedServitor(slots)
|
|
|
|
internal.maybeAddOnTheMend(slots, playerColor)
|
|
|
|
internal.maybeAddRealityAcidReference(slots)
|
|
|
|
local bondList = internal.extractBondedCards(slots)
|
|
|
|
internal.checkTaboos(deck.taboo_id, slots, playerColor)
|
|
|
|
internal.maybeAddUpgradeSheets(slots)
|
|
|
|
|
|
|
|
-- get upgrades for customizable cards
|
|
|
|
local customizations = {}
|
|
|
|
if deck.meta then
|
|
|
|
customizations = JSON.decode(deck.meta)
|
|
|
|
end
|
|
|
|
|
|
|
|
callback(slots, deck.investigator_code, bondList, customizations, playerColor, loadAltInvestigator)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Checks to see if the slot list includes the random weakness ID. If it does,
|
|
|
|
-- removes it from the deck and replaces it with the ID of a random basic weakness provided by the
|
|
|
|
-- all cards bag
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
--- of those cards which will be spawned
|
|
|
|
---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast
|
|
|
|
--- if a weakness is added.
|
|
|
|
internal.maybeDrawRandomWeakness = function(slots, playerColor)
|
|
|
|
local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0
|
|
|
|
slots[RANDOM_WEAKNESS_ID] = nil
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
if randomWeaknessAmount > 0 then
|
|
|
|
for i = 1, randomWeaknessAmount do
|
2024-06-09 10:10:21 -04:00
|
|
|
local weaknessId = allCardsBagApi.getRandomWeaknessId()
|
|
|
|
slots[weaknessId] = (slots[weaknessId] or 0) + 1
|
|
|
|
end
|
|
|
|
internal.maybePrint("Added " .. randomWeaknessAmount .. " random basic weakness(es) to deck", playerColor)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
|
|
|
|
---@param deck table The processed ArkhamDB deck response
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the
|
|
|
|
--- number of those cards which will be spawned
|
|
|
|
---@return string: Contains the name of the art that should be loaded ("normal", "promo" or "revised")
|
|
|
|
internal.addInvestigatorCards = function(deck, slots)
|
|
|
|
local investigatorId = deck.investigator_code
|
|
|
|
slots[investigatorId .. "-m"] = 1
|
|
|
|
local deckMeta = JSON.decode(deck.meta)
|
|
|
|
|
|
|
|
-- handling alternative investigator art and parallel investigators
|
|
|
|
local loadAltInvestigator = "normal"
|
|
|
|
if deckMeta ~= nil then
|
|
|
|
local altFrontId = tonumber(deckMeta.alternate_front) or 0
|
|
|
|
local altBackId = tonumber(deckMeta.alternate_back) or 0
|
|
|
|
local altArt = { front = "normal", back = "normal" }
|
|
|
|
|
|
|
|
-- translating front ID
|
|
|
|
if altFrontId > 90000 and altFrontId < 90100 then
|
|
|
|
altArt.front = "parallel"
|
|
|
|
elseif altFrontId > 01500 and altFrontId < 01506 then
|
|
|
|
altArt.front = "revised"
|
|
|
|
elseif altFrontId > 98000 then
|
|
|
|
altArt.front = "promo"
|
|
|
|
end
|
|
|
|
|
|
|
|
-- translating back ID
|
|
|
|
if altBackId > 90000 and altBackId < 90100 then
|
|
|
|
altArt.back = "parallel"
|
|
|
|
elseif altBackId > 01500 and altBackId < 01506 then
|
|
|
|
altArt.back = "revised"
|
|
|
|
elseif altBackId > 98000 then
|
|
|
|
altArt.back = "promo"
|
|
|
|
end
|
|
|
|
|
|
|
|
-- updating investigatorID based on alt investigator selection
|
|
|
|
-- precedence: parallel > promo > revised
|
|
|
|
if altArt.front == "parallel" then
|
|
|
|
if altArt.back == "parallel" then
|
|
|
|
investigatorId = investigatorId .. "-p"
|
|
|
|
else
|
|
|
|
investigatorId = investigatorId .. "-pf"
|
|
|
|
end
|
|
|
|
elseif altArt.back == "parallel" then
|
|
|
|
investigatorId = investigatorId .. "-pb"
|
|
|
|
elseif altArt.front == "promo" or altArt.back == "promo" then
|
|
|
|
loadAltInvestigator = "promo"
|
|
|
|
elseif altArt.front == "revised" or altArt.back == "revised" then
|
|
|
|
loadAltInvestigator = "revised"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
slots[investigatorId] = 1
|
|
|
|
deck.investigator_code = investigatorId
|
|
|
|
return loadAltInvestigator
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Process the card list looking for the customizable cards, and add their upgrade sheets if needed
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
-- of those cards which will be spawned
|
|
|
|
internal.maybeAddUpgradeSheets = function(slots)
|
|
|
|
for cardId, _ in pairs(slots) do
|
|
|
|
-- upgrade sheets for customizable cards
|
|
|
|
local upgradesheet = allCardsBagApi.getCardById(cardId .. "-c")
|
|
|
|
if upgradesheet ~= nil then
|
|
|
|
slots[cardId .. "-c"] = 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Process the card list looking for the Summoned Servitor, and add its minicard to the list if
|
|
|
|
-- needed
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
-- of those cards which will be spawned
|
|
|
|
internal.maybeAddSummonedServitor = function(slots)
|
|
|
|
if slots["09080"] ~= nil then
|
|
|
|
slots["09080-m"] = 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update
|
|
|
|
-- the count based on the investigator count
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
-- of those cards which will be spawned
|
|
|
|
---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast if an error occurs
|
|
|
|
internal.maybeAddOnTheMend = function(slots, playerColor)
|
|
|
|
if slots["09006"] ~= nil then
|
|
|
|
local investigatorCount = playAreaApi.getInvestigatorCount()
|
|
|
|
if investigatorCount ~= nil then
|
|
|
|
slots["09006"] = investigatorCount
|
|
|
|
else
|
|
|
|
internal.maybePrint("Something went wrong with the load, adding 4 copies of On the Mend", playerColor)
|
|
|
|
slots["09006"] = 4
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Process the card list looking for Reality Acid and adds the reference sheet when needed
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
-- of those cards which will be spawned
|
|
|
|
internal.maybeAddRealityAcidReference = function(slots)
|
|
|
|
if slots["89004"] ~= nil then
|
|
|
|
slots["89005"] = 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Processes the deck description from ArkhamDB and modifies the slot list accordingly
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number
|
|
|
|
---@param description string The deck desription from ArkhamDB
|
|
|
|
internal.maybeModifyDeckFromDescription = function(slots, description, playerColor)
|
|
|
|
-- check for import instructions
|
|
|
|
local pos = string.find(description, "++SCED import instructions++")
|
|
|
|
if not pos then return end
|
|
|
|
|
|
|
|
-- remove everything before instructions
|
|
|
|
local tempStr = string.sub(description, pos)
|
|
|
|
|
|
|
|
-- parse each line in instructions
|
|
|
|
for line in tempStr:gmatch("([^\n]+)") do
|
|
|
|
-- remove dashes at the start
|
|
|
|
line = line:gsub("%- ", "")
|
|
|
|
|
|
|
|
-- remove spaces
|
|
|
|
line = line:gsub("%s", "")
|
|
|
|
|
|
|
|
-- remove balanced brackets
|
|
|
|
line = line:gsub("%b()", "")
|
|
|
|
line = line:gsub("%b[]", "")
|
|
|
|
|
|
|
|
-- get instructor
|
|
|
|
local instructor = ""
|
|
|
|
for word in line:gmatch("%a+:") do
|
|
|
|
instructor = word
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
-- go to the next line if no valid instructor found
|
|
|
|
if instructor ~= "add:" and instructor ~= "remove:" then
|
|
|
|
goto nextLine
|
|
|
|
end
|
|
|
|
|
|
|
|
-- remove instructor from line
|
|
|
|
line = line:gsub(instructor, "")
|
|
|
|
|
|
|
|
-- evaluate instructions
|
|
|
|
for str in line:gmatch("([^,]+)") do
|
|
|
|
if instructor == "add:" then
|
|
|
|
slots[str] = (slots[str] or 0) + 1
|
|
|
|
elseif instructor == "remove:" then
|
|
|
|
if slots[str] == nil then
|
|
|
|
internal.maybePrint("Tried to remove card ID " .. str .. ", but didn't find card in deck.", playerColor)
|
|
|
|
else
|
|
|
|
slots[str] = math.max(slots[str] - 1, 0)
|
|
|
|
|
|
|
|
-- fully remove cards that have a quantity of 0
|
|
|
|
if slots[str] == 0 then
|
|
|
|
slots[str] = nil
|
|
|
|
|
|
|
|
-- also remove related minicard
|
|
|
|
slots[str .. "-m"] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- jump mark at the end of the loop
|
|
|
|
::nextLine::
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
|
|
|
|
internal.extractBondedCards = function(slots)
|
|
|
|
-- Create a list of bonded cards first so we don't modify slots while iterating
|
|
|
|
local bondedCards = { }
|
|
|
|
local bondedList = { }
|
|
|
|
for cardId, cardCount in pairs(slots) do
|
|
|
|
local card = allCardsBagApi.getCardById(cardId)
|
|
|
|
if card ~= nil and card.metadata.bonded ~= nil then
|
|
|
|
for _, bond in ipairs(card.metadata.bonded) do
|
|
|
|
-- 'unlimited' upper limit for cards without this data
|
|
|
|
local maxCount = bond.maxCount or 99
|
|
|
|
|
|
|
|
-- add a bonded card for each copy of the parent card (until the max value is reached)
|
|
|
|
bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount)
|
|
|
|
|
|
|
|
-- We need to know which cards are bonded to determine their position, remember them
|
|
|
|
bondedList[bond.id] = true
|
|
|
|
|
|
|
|
-- Also adding taboo versions of bonded cards to the list
|
|
|
|
bondedList[bond.id .. "-t"] = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Add any bonded cards to the main slots list
|
|
|
|
for bondedId, bondedCount in pairs(bondedCards) do
|
|
|
|
slots[bondedId] = bondedCount
|
|
|
|
end
|
|
|
|
|
|
|
|
return bondedList
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. "XXXX" becomes "XXXX-t")
|
|
|
|
---@param tabooId string The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used
|
|
|
|
---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
|
|
|
|
internal.checkTaboos = function(tabooId, slots, playerColor)
|
|
|
|
if tabooId then
|
|
|
|
for cardId, _ in pairs(tabooList[tabooId].cards) do
|
|
|
|
if slots[cardId] ~= nil then
|
|
|
|
-- Make sure there's a taboo version of the card before we replace it
|
|
|
|
-- SCED only maintains the most recent taboo cards. If a deck is using
|
|
|
|
-- an older taboo list it's possible the card isn't a taboo any more
|
|
|
|
local tabooCard = allCardsBagApi.getCardById(cardId .. "-t")
|
|
|
|
if tabooCard == nil then
|
|
|
|
local basicCard = allCardsBagApi.getCardById(cardId)
|
|
|
|
internal.maybePrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version", playerColor)
|
|
|
|
else
|
|
|
|
slots[cardId .. "-t"] = slots[cardId]
|
|
|
|
slots[cardId] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
internal.maybePrint = function(message, playerColor)
|
|
|
|
if playerColor ~= "None" then
|
|
|
|
printToAll(message, playerColor)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Gets the ArkhamDB config info from the configuration object.
|
|
|
|
---@return table: configuration data
|
|
|
|
internal.getConfiguration = function()
|
|
|
|
local configuration = getObjectsWithTag("import_configuration_provider")[1].getTable("configuration")
|
|
|
|
printPriority = configuration.priority
|
|
|
|
return configuration
|
|
|
|
end
|
|
|
|
|
|
|
|
internal.fixUtf16String = function(str)
|
|
|
|
return str:gsub("\\u(%w%w%w%w)", function(match)
|
|
|
|
return string.char(tonumber(match, 16))
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
Request = {
|
|
|
|
is_done = false,
|
|
|
|
is_successful = false
|
|
|
|
}
|
|
|
|
|
|
|
|
-- Creates a new instance of a Request. Should not be directly called. Instead use Request.start() and Request.deferred().
|
|
|
|
---@param uri table
|
|
|
|
---@param configure fun(request, status)
|
|
|
|
---@return Request
|
|
|
|
function Request:new(uri, configure)
|
|
|
|
local this = {}
|
|
|
|
|
|
|
|
setmetatable(this, self)
|
|
|
|
self.__index = self
|
|
|
|
|
|
|
|
if type(uri) == "table" then
|
|
|
|
uri = table.concat(uri, "/")
|
|
|
|
end
|
|
|
|
|
|
|
|
this.uri = uri
|
|
|
|
WebRequest.get(uri, function(status) configure(this, status) end)
|
|
|
|
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
|
|
|
|
-- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)
|
|
|
|
---@param uri table
|
|
|
|
---@param on_success fun(request, status, vararg)
|
|
|
|
---@param on_error fun(status)|nil
|
|
|
|
---@return Request
|
|
|
|
function Request.deferred(uri, on_success, on_error, ...)
|
|
|
|
local parameters = table.pack(...)
|
|
|
|
return Request:new(uri, function(request, status)
|
|
|
|
if (status.is_done) then
|
|
|
|
if (status.is_error) then
|
|
|
|
request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
|
|
|
|
request.is_successful = false
|
|
|
|
request.is_done = true
|
|
|
|
else
|
|
|
|
on_success(request, status)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Creates a new request. on_success should return whether the resultant data is as expected, and the processed content of the request.
|
|
|
|
---@param uri table
|
|
|
|
---@param on_success fun(status, vararg): boolean, any
|
|
|
|
---@param on_error nil|fun(status, vararg): string
|
|
|
|
---@return Request
|
|
|
|
function Request.start(uri, on_success, on_error, ...)
|
|
|
|
local parameters = table.pack(...)
|
|
|
|
return Request.deferred(uri, function(request, status)
|
|
|
|
local result, message = on_success(status, table.unpack(parameters))
|
|
|
|
if not result then request.error_message = message else request.content = message end
|
|
|
|
request.is_successful = result
|
|
|
|
request.is_done = true
|
|
|
|
end, on_error, table.unpack(parameters))
|
|
|
|
end
|
|
|
|
|
|
|
|
---@param requests Request[]
|
|
|
|
---@param on_success fun(content: any, vararg: any)
|
|
|
|
---@param on_error fun(requests: Request, vararg: any)|nil
|
|
|
|
function Request.with_all(requests, on_success, on_error, ...)
|
|
|
|
local parameters = table.pack(...)
|
|
|
|
|
|
|
|
Wait.condition(function()
|
|
|
|
local results = {}
|
|
|
|
local errors = {}
|
|
|
|
|
|
|
|
for _, request in ipairs(requests) do
|
|
|
|
if request.is_successful then
|
|
|
|
table.insert(results, request.content)
|
|
|
|
else
|
|
|
|
table.insert(errors, request)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if (#errors <= 0) then
|
|
|
|
on_success(results, table.unpack(parameters))
|
|
|
|
elseif on_error == nil then
|
|
|
|
for _, request in ipairs(errors) do
|
|
|
|
internal.maybePrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }))
|
|
|
|
end
|
|
|
|
else
|
|
|
|
on_error(requests, table.unpack(parameters))
|
|
|
|
end
|
|
|
|
end, function()
|
|
|
|
for _, request in ipairs(requests) do
|
|
|
|
if not request.is_done then return false end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Request:with(callback, ...)
|
|
|
|
local arguments = table.pack(...)
|
|
|
|
Wait.condition(function()
|
|
|
|
if self.is_successful then
|
|
|
|
callback(self.content, table.unpack(arguments))
|
|
|
|
end
|
|
|
|
end, function() return self.is_done
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
return ArkhamDb
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
__bundle_register("playercards/PlayerCardPanel", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
---@diagnostic disable: param-type-mismatch
|
|
|
|
require("playercards/PlayerCardPanelData")
|
|
|
|
|
|
|
|
local allCardsBagApi = require("playercards/AllCardsBagApi")
|
|
|
|
local arkhamDb = require("arkhamdb/ArkhamDb")
|
|
|
|
local spawnBag = require("playercards/SpawnBag")
|
|
|
|
|
|
|
|
local lastWeaknessTrait = "Madness"
|
|
|
|
|
|
|
|
-- Size and position information for the three rows of class buttons
|
|
|
|
local CIRCLE_BUTTON_SIZE = 250
|
|
|
|
local CLASS_BUTTONS_X_OFFSET = 0.1325
|
|
|
|
local INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)
|
|
|
|
local LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)
|
|
|
|
local UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)
|
|
|
|
|
|
|
|
-- Size and position information for the two blocks of other buttons
|
|
|
|
local MISC_BUTTONS_X_OFFSET = 0.155
|
|
|
|
local WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)
|
|
|
|
local OTHER_ROW_START = Vector(0.605, 0.1, 0.666)
|
|
|
|
|
|
|
|
-- Size and position information for the Cycle (box) buttons
|
|
|
|
local CYCLE_BUTTON_SIZE = 468
|
|
|
|
local CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)
|
|
|
|
local CYCLE_COLUMN_COUNT = 3
|
|
|
|
local CYCLE_BUTTONS_X_OFFSET = 0.267
|
|
|
|
local CYCLE_BUTTONS_Z_OFFSET = 0.2665
|
|
|
|
|
|
|
|
local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }
|
|
|
|
local TRANSPARENT = { 0, 0, 0, 0 }
|
|
|
|
|
|
|
|
local FACE_UP_ROTATION = { x = 0, y = 270, z = 0 }
|
|
|
|
local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180 }
|
|
|
|
|
|
|
|
-- ---------- IMPORTANT ----------
|
|
|
|
-- Coordinates defined below are in global dimensions relative to the panel - DO NOT USE THESE
|
|
|
|
-- DIRECTLY. Call scalePositions() before use, and reference the variables below
|
|
|
|
|
|
|
|
-- Layout width for a single card, in global coordinate space
|
|
|
|
local CARD_WIDTH = 2.3
|
|
|
|
|
|
|
|
-- Coordinates to begin laying out cards. These vary based on the cards that are being placed by
|
|
|
|
-- considering the width of the cards, number of cards, and desired spread intervals.
|
|
|
|
-- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y
|
|
|
|
-- coordinates on these provide global disances while the Z is local.
|
|
|
|
local START_POSITIONS = {
|
|
|
|
classCards = Vector(CARD_WIDTH * 9.5, 2, 1.4),
|
|
|
|
investigator = Vector(6 * 2.5, 2, 1.3),
|
|
|
|
cycle = Vector(CARD_WIDTH * 9.5, 2, 2.4),
|
|
|
|
other = Vector(CARD_WIDTH * 9.5, 2, 1.4),
|
|
|
|
randomWeakness = Vector(0, 2, 1.4),
|
|
|
|
-- Because the card spread is handled by the SpawnBag, we don't know (programatically) where this
|
|
|
|
-- should be placed. If more customizable cards are added it will need to be moved.
|
|
|
|
summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7)
|
|
|
|
}
|
|
|
|
|
|
|
|
-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out
|
|
|
|
local CARD_ROW_OFFSET = 3.7
|
|
|
|
local CARD_GROUP_OFFSET = 2
|
|
|
|
|
|
|
|
-- Position offsets for investigator decks in investigator mode, defines the spacing for how the
|
|
|
|
-- rows and columns are laid out
|
|
|
|
local INVESTIGATOR_POSITION_SHIFT_ROW = Vector(0, 0, 11)
|
|
|
|
local INVESTIGATOR_POSITION_SHIFT_COL = Vector(-6, 0, 0)
|
|
|
|
local INVESTIGATOR_MAX_COLS = 6
|
|
|
|
|
|
|
|
-- Positions relative to the minicard to place other stacks. Both signature card piles and starter
|
|
|
|
-- decks use SIGNATURE_OFFSET
|
|
|
|
local INVESTIGATOR_CARD_OFFSET = Vector(0, 0, 2.55)
|
|
|
|
local INVESTIGATOR_SIGNATURE_OFFSET = Vector(0, 0, 5.75)
|
|
|
|
|
|
|
|
-- USE THESE! Positions and offset shifts accounting for the scale of the panel
|
|
|
|
local startPositions
|
|
|
|
local cardRowOffset
|
|
|
|
local cardGroupOffset
|
|
|
|
local investigatorPositionShiftRow
|
|
|
|
local investigatorPositionShiftCol
|
|
|
|
local investigatorCardOffset
|
|
|
|
local investigatorSignatureOffset
|
|
|
|
|
|
|
|
local CLASS_LIST = {
|
|
|
|
"Guardian",
|
|
|
|
"Seeker",
|
|
|
|
"Rogue",
|
|
|
|
"Mystic",
|
|
|
|
"Survivor",
|
|
|
|
"Neutral"
|
|
|
|
}
|
|
|
|
local CYCLE_LIST = {
|
|
|
|
"Core",
|
|
|
|
"The Dunwich Legacy",
|
|
|
|
"The Path to Carcosa",
|
|
|
|
"The Forgotten Age",
|
|
|
|
"The Circle Undone",
|
|
|
|
"The Dream-Eaters",
|
|
|
|
"The Innsmouth Conspiracy",
|
|
|
|
"Edge of the Earth",
|
|
|
|
"The Scarlet Keys",
|
|
|
|
"The Feast of Hemlock Vale",
|
|
|
|
"Investigator Packs"
|
|
|
|
}
|
|
|
|
|
|
|
|
local excludedNonBasicWeaknesses
|
|
|
|
local spawnStarterDecks = false
|
|
|
|
local helpVisibleToPlayers = {}
|
|
|
|
|
|
|
|
function onSave()
|
|
|
|
return JSON.encode({ spawnBagState = spawnBag.getStateForSave() })
|
|
|
|
end
|
|
|
|
|
|
|
|
function onLoad(savedData)
|
|
|
|
if savedData and savedData ~= "" then
|
|
|
|
local saveState = JSON.decode(savedData) or {}
|
|
|
|
if saveState.spawnBagState ~= nil then
|
|
|
|
spawnBag.loadFromSave(saveState.spawnBagState)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
arkhamDb.initialize()
|
|
|
|
buildExcludedWeaknessList()
|
|
|
|
createButtons()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Build a list of non-basic weaknesses which should be excluded from the last weakness set,
|
|
|
|
-- including all signature cards and evolved weaknesses.
|
|
|
|
function buildExcludedWeaknessList()
|
|
|
|
excludedNonBasicWeaknesses = {}
|
|
|
|
for _, investigator in pairs(INVESTIGATORS) do
|
|
|
|
for _, signatureId in ipairs(investigator.signatures) do
|
|
|
|
excludedNonBasicWeaknesses[signatureId] = true
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
for _, weaknessId in ipairs(EVOLVED_WEAKNESSES) do
|
|
|
|
excludedNonBasicWeaknesses[weaknessId] = true
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createButtons()
|
|
|
|
createHelpButton()
|
|
|
|
createInvestigatorButtons()
|
|
|
|
createLevelZeroButtons()
|
|
|
|
createUpgradedButtons()
|
|
|
|
createWeaknessButtons()
|
|
|
|
createOtherButtons()
|
|
|
|
createCycleButtons()
|
|
|
|
createClearButton()
|
|
|
|
-- Create investigator mode buttons last so the indexes are set when we need to update them
|
|
|
|
createInvestigatorModeButtons()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createHelpButton()
|
|
|
|
self.createButton({
|
|
|
|
function_owner = self,
|
|
|
|
click_function = "toggleHelp",
|
|
|
|
position = Vector(0.845, 0.1, -0.855),
|
|
|
|
height = 180,
|
|
|
|
width = 180,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
})
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createInvestigatorButtons()
|
|
|
|
local invButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CIRCLE_BUTTON_SIZE,
|
|
|
|
width = CIRCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = INVESTIGATOR_ROW_START:copy()
|
|
|
|
for _, class in ipairs(CLASS_LIST) do
|
|
|
|
invButtonParams.click_function = "spawnInvestigators" .. class
|
|
|
|
invButtonParams.position = buttonPos
|
|
|
|
self.createButton(invButtonParams)
|
|
|
|
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
|
|
|
|
self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createLevelZeroButtons()
|
|
|
|
local l0ButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CIRCLE_BUTTON_SIZE,
|
|
|
|
width = CIRCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = LEVEL_ZERO_ROW_START:copy()
|
|
|
|
for _, class in ipairs(CLASS_LIST) do
|
|
|
|
l0ButtonParams.click_function = "spawnBasic" .. class
|
|
|
|
l0ButtonParams.position = buttonPos
|
|
|
|
self.createButton(l0ButtonParams)
|
|
|
|
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
|
|
|
|
self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createUpgradedButtons()
|
|
|
|
local upgradedButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CIRCLE_BUTTON_SIZE,
|
|
|
|
width = CIRCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = UPGRADED_ROW_START:copy()
|
|
|
|
for _, class in ipairs(CLASS_LIST) do
|
|
|
|
upgradedButtonParams.click_function = "spawnUpgraded" .. class
|
|
|
|
upgradedButtonParams.position = buttonPos
|
|
|
|
self.createButton(upgradedButtonParams)
|
|
|
|
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
|
|
|
|
self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createWeaknessButtons()
|
|
|
|
local weaknessButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CIRCLE_BUTTON_SIZE,
|
|
|
|
width = CIRCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = WEAKNESS_ROW_START:copy()
|
|
|
|
weaknessButtonParams.click_function = "spawnWeaknesses"
|
|
|
|
weaknessButtonParams.tooltip = "All Weaknesses"
|
|
|
|
weaknessButtonParams.position = buttonPos
|
|
|
|
self.createButton(weaknessButtonParams)
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
|
|
|
|
weaknessButtonParams.click_function = "spawnRandomWeakness"
|
|
|
|
weaknessButtonParams.tooltip = "Random Basic Weakness\nRight-click to specify a trait"
|
|
|
|
weaknessButtonParams.position = buttonPos
|
|
|
|
self.createButton(weaknessButtonParams)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createOtherButtons()
|
|
|
|
local otherButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CIRCLE_BUTTON_SIZE,
|
|
|
|
width = CIRCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = OTHER_ROW_START:copy()
|
|
|
|
otherButtonParams.click_function = "spawnBonded"
|
|
|
|
otherButtonParams.tooltip = "Bonded Cards"
|
|
|
|
otherButtonParams.position = buttonPos
|
|
|
|
self.createButton(otherButtonParams)
|
|
|
|
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
|
|
|
|
otherButtonParams.click_function = "spawnUpgradeSheets"
|
|
|
|
otherButtonParams.tooltip = "Customization Upgrade Sheets"
|
|
|
|
otherButtonParams.position = buttonPos
|
|
|
|
self.createButton(otherButtonParams)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createCycleButtons()
|
|
|
|
local cycleButtonParams = {
|
|
|
|
function_owner = self,
|
|
|
|
height = CYCLE_BUTTON_SIZE,
|
|
|
|
width = CYCLE_BUTTON_SIZE,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
}
|
|
|
|
local buttonPos = CYCLE_BUTTON_START:copy()
|
|
|
|
local rowCount = 0
|
|
|
|
local colCount = 0
|
|
|
|
for _, cycle in ipairs(CYCLE_LIST) do
|
|
|
|
cycleButtonParams.click_function = "spawnCycle" .. cycle
|
|
|
|
cycleButtonParams.position = buttonPos
|
|
|
|
cycleButtonParams.tooltip = cycle
|
|
|
|
self.createButton(cycleButtonParams)
|
|
|
|
self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)
|
|
|
|
colCount = colCount + 1
|
|
|
|
-- If we've reached the end of a row, shift down and back to the first column
|
|
|
|
if colCount >= CYCLE_COLUMN_COUNT then
|
|
|
|
buttonPos = CYCLE_BUTTON_START:copy()
|
|
|
|
rowCount = rowCount + 1
|
|
|
|
colCount = 0
|
|
|
|
buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount
|
|
|
|
if rowCount == 3 then
|
|
|
|
-- Account for two centered buttons on the final row
|
|
|
|
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2
|
|
|
|
-- Account for centered button on the final row
|
|
|
|
-- buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
else
|
|
|
|
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createClearButton()
|
|
|
|
self.createButton({
|
|
|
|
function_owner = self,
|
|
|
|
click_function = "deleteAll",
|
|
|
|
position = Vector(0, 0.1, 0.852),
|
|
|
|
height = 170,
|
|
|
|
width = 750,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = TRANSPARENT
|
|
|
|
})
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createInvestigatorModeButtons()
|
|
|
|
self.createButton({
|
|
|
|
function_owner = self,
|
|
|
|
click_function = "setCardsOnlyMode",
|
|
|
|
position = Vector(0.251, 0.1, -0.322),
|
|
|
|
height = 170,
|
|
|
|
width = 760,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = spawnStarterDecks and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR
|
|
|
|
})
|
|
|
|
self.createButton({
|
|
|
|
function_owner = self,
|
|
|
|
click_function = "setspawnStarterDecks",
|
|
|
|
position = Vector(0.66, 0.1, -0.322),
|
|
|
|
height = 170,
|
|
|
|
width = 760,
|
|
|
|
scale = Vector(0.25, 1, 0.25),
|
|
|
|
color = spawnStarterDecks and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT
|
|
|
|
})
|
|
|
|
local checkX = spawnStarterDecks and 0.52 or 0.11
|
|
|
|
self.createButton({
|
|
|
|
function_owner = self,
|
|
|
|
label = "✓",
|
|
|
|
click_function = "doNothing",
|
|
|
|
position = Vector(checkX, 0.11, -0.317),
|
|
|
|
height = 0,
|
|
|
|
width = 0,
|
|
|
|
font_size = 300,
|
|
|
|
scale = Vector(0.1, 1, 0.1),
|
|
|
|
font_color = { 0, 0, 0 },
|
|
|
|
color = { 1, 1, 1 }
|
|
|
|
})
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function createXML(showOtherCardsButton)
|
|
|
|
-- basic XML for the help button
|
|
|
|
local xmlTable = {
|
|
|
|
{
|
|
|
|
tag = "Panel",
|
|
|
|
attributes = {
|
|
|
|
active = "false",
|
|
|
|
id = "helpPanel",
|
|
|
|
position = "-165 -70 -2",
|
|
|
|
rotation = "0 0 180",
|
|
|
|
height = "50",
|
|
|
|
width = "107",
|
|
|
|
color = "#00000099"
|
|
|
|
},
|
|
|
|
children = {
|
|
|
|
tag = "Text",
|
|
|
|
attributes = {
|
|
|
|
id = "helpText",
|
|
|
|
rectAlignment = "MiddleCenter",
|
|
|
|
height = "480",
|
|
|
|
width = "1000",
|
|
|
|
scale = "0.1 0.1 1",
|
|
|
|
fontSize = "66",
|
|
|
|
color = "#F5F5F5",
|
|
|
|
backgroundColor = "#FF0000",
|
|
|
|
alignment = "MiddleLeft",
|
|
|
|
horizontalOverflow = "wrap",
|
|
|
|
text = "• Select a group to place cards\n" ..
|
|
|
|
"• Copy the cards you want for your deck\n" ..
|
|
|
|
"• Select a new group to clear the placed cards and see new ones\n" ..
|
|
|
|
"• Clear to remove all cards"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
}
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- add the "Additional Cards" button if cards without cycle were detected
|
|
|
|
if showOtherCardsButton then
|
|
|
|
local otherCardsButtonXml = {
|
|
|
|
tag = "Panel",
|
|
|
|
attributes = {
|
|
|
|
position = "44.25 65.75 -11",
|
|
|
|
rotation = "0 0 180",
|
|
|
|
height = "225",
|
|
|
|
width = "225",
|
|
|
|
scale = "0.1 0.1 1",
|
|
|
|
onClick = "spawnOtherCards"
|
|
|
|
},
|
|
|
|
children = {
|
|
|
|
tag = "Image",
|
|
|
|
attributes = { image = "OtherCards" }
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
}
|
2024-07-27 21:47:52 -04:00
|
|
|
table.insert(xmlTable, otherCardsButtonXml)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
helpVisibleToPlayers = {}
|
|
|
|
self.UI.setXmlTable(xmlTable)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- click function for the XML button for the additional player cards
|
|
|
|
function spawnOtherCards()
|
|
|
|
spawnCycle("Other")
|
|
|
|
end
|
|
|
|
|
|
|
|
function toggleHelp(_, playerColor, _)
|
|
|
|
if helpVisibleToPlayers[playerColor] then
|
|
|
|
helpVisibleToPlayers[playerColor] = nil
|
|
|
|
else
|
|
|
|
helpVisibleToPlayers[playerColor] = true
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
updateHelpVisibility()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function updateHelpVisibility()
|
|
|
|
local visibility = ""
|
|
|
|
for player, _ in pairs(helpVisibleToPlayers) do
|
|
|
|
if string.len(visibility) > 0 then
|
|
|
|
visibility = visibility .. "|" .. player
|
2024-06-09 10:10:21 -04:00
|
|
|
else
|
2024-07-27 21:47:52 -04:00
|
|
|
visibility = player
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
self.UI.setAttribute("helpText", "visibility", visibility)
|
|
|
|
self.UI.setAttribute("helpPanel", "visibility", visibility)
|
|
|
|
self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function setspawnStarterDecks()
|
|
|
|
spawnStarterDecks = true
|
|
|
|
updateStarterModeButtons()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function setCardsOnlyMode()
|
|
|
|
spawnStarterDecks = false
|
|
|
|
updateStarterModeButtons()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function updateStarterModeButtons()
|
|
|
|
local buttonCount = #self.getButtons()
|
|
|
|
-- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size
|
|
|
|
self.removeButton(buttonCount - 1)
|
|
|
|
self.removeButton(buttonCount - 2)
|
|
|
|
self.removeButton(buttonCount - 3)
|
|
|
|
createInvestigatorModeButtons()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Clears the table and updates positions based on scale (should be called before ANY card placement)
|
|
|
|
function prepareToPlaceCards()
|
|
|
|
deleteAll()
|
|
|
|
scalePositions()
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Updates the positions based on the current object scale to ensure the relative layout functions
|
|
|
|
-- properly at different scales.
|
|
|
|
function scalePositions()
|
|
|
|
-- Assume scaling is consistent in X and Z dimensions
|
|
|
|
local scale = 1 / self.getScale().x
|
|
|
|
startPositions = {}
|
|
|
|
for key, pos in pairs(START_POSITIONS) do
|
|
|
|
-- Because a scaled object means a different global size, using global distance for Z results in
|
|
|
|
-- the cards being closer or farther depending on the scale. Leave the Z values and only scale X and Y
|
|
|
|
startPositions[key] = Vector(pos)
|
|
|
|
startPositions[key].x = startPositions[key].x * scale
|
|
|
|
startPositions[key].y = startPositions[key].y * scale
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
cardRowOffset = CARD_ROW_OFFSET * scale
|
|
|
|
cardGroupOffset = CARD_GROUP_OFFSET * scale
|
|
|
|
investigatorPositionShiftRow = Vector(INVESTIGATOR_POSITION_SHIFT_ROW):scale(scale)
|
|
|
|
investigatorPositionShiftCol = Vector(INVESTIGATOR_POSITION_SHIFT_COL):scale(scale)
|
|
|
|
investigatorCardOffset = Vector(INVESTIGATOR_CARD_OFFSET):scale(scale)
|
|
|
|
investigatorSignatureOffset = Vector(INVESTIGATOR_SIGNATURE_OFFSET):scale(scale)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Deletes all cards currently placed on the table
|
|
|
|
function deleteAll()
|
|
|
|
spawnBag.recall(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Spawn an investigator group, based on the current UI setting for either investigators or starter decks
|
|
|
|
---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData
|
|
|
|
function spawnInvestigatorGroup(groupName)
|
|
|
|
prepareToPlaceCards()
|
|
|
|
Wait.frames(function()
|
|
|
|
if spawnStarterDecks then
|
|
|
|
spawnStarters(groupName)
|
|
|
|
else
|
|
|
|
spawnInvestigators(groupName)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end, 2)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Spawn cards for all investigators in the given group. This creates piles for all defined
|
|
|
|
-- investigator cards and minicards as well as the signature cards.
|
|
|
|
---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData
|
|
|
|
function spawnInvestigators(groupName)
|
|
|
|
if INVESTIGATOR_GROUPS[groupName] == nil then
|
|
|
|
printToAll("No investigator data for " .. groupName .. " yet")
|
|
|
|
return
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local col = 1
|
|
|
|
local row = 1
|
|
|
|
local investigatorCount = #INVESTIGATOR_GROUPS[groupName]
|
|
|
|
local position = getInvestigatorRowStartPos(investigatorCount, row)
|
|
|
|
|
|
|
|
for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do
|
|
|
|
for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do
|
|
|
|
spawnBag.spawn(spawnSpec)
|
|
|
|
end
|
|
|
|
position:add(investigatorPositionShiftCol)
|
|
|
|
col = col + 1
|
|
|
|
if col > INVESTIGATOR_MAX_COLS then
|
|
|
|
col = 1
|
|
|
|
row = row + 1
|
|
|
|
position = getInvestigatorRowStartPos(investigatorCount, row)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function getInvestigatorRowStartPos(investigatorCount, row)
|
|
|
|
local rowStart = Vector(startPositions.investigator)
|
|
|
|
rowStart:add(Vector(
|
|
|
|
investigatorPositionShiftRow.x * (row - 1),
|
|
|
|
investigatorPositionShiftRow.y * (row - 1),
|
|
|
|
investigatorPositionShiftRow.z * (row - 1)))
|
|
|
|
local investigatorsInRow = math.min(investigatorCount - INVESTIGATOR_MAX_COLS * (row - 1), INVESTIGATOR_MAX_COLS)
|
|
|
|
rowStart:add(Vector(
|
|
|
|
investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,
|
|
|
|
investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,
|
|
|
|
investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2))
|
|
|
|
return rowStart
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Creates the spawn spec for the investigator's signature cards.
|
|
|
|
---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData
|
|
|
|
---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS
|
|
|
|
---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below
|
|
|
|
function buildInvestigatorSpawnSpec(investigatorName, investigatorData, position)
|
|
|
|
local sigPos = Vector(position):add(investigatorSignatureOffset)
|
|
|
|
local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position)
|
|
|
|
table.insert(spawns, {
|
|
|
|
name = investigatorName .. "signatures",
|
|
|
|
cards = investigatorData.signatures,
|
|
|
|
globalPos = self.positionToWorld(sigPos),
|
|
|
|
rotation = FACE_UP_ROTATION
|
|
|
|
})
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
return spawns
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Builds the spawn specs for minicards and investigator cards. These are common enough to be
|
|
|
|
-- shared, and will only differ in whether they spawn the full stack of possible investigator and
|
|
|
|
-- minicards, or only the first of each.
|
|
|
|
---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData
|
|
|
|
---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS
|
|
|
|
---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below
|
|
|
|
---@param oneCardOnly? boolean If true, will spawn only the first card in the investigator card
|
|
|
|
--- and minicard lists. Otherwise, spawn them all in a deck
|
|
|
|
function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly)
|
|
|
|
local cardPos = Vector(position):add(investigatorCardOffset)
|
|
|
|
return {
|
|
|
|
{
|
|
|
|
name = investigatorName .. "minicards",
|
|
|
|
cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,
|
|
|
|
globalPos = self.positionToWorld(position),
|
|
|
|
rotation = FACE_UP_ROTATION
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name = investigatorName .. "cards",
|
|
|
|
cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,
|
|
|
|
globalPos = self.positionToWorld(cardPos),
|
|
|
|
rotation = FACE_UP_ROTATION
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawns all starter decks (single minicard and investigator card, plus the starter deck) for
|
|
|
|
-- investigators in the given group.
|
|
|
|
---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData
|
|
|
|
function spawnStarters(groupName)
|
|
|
|
local col = 1
|
|
|
|
local row = 1
|
|
|
|
local investigatorCount = #INVESTIGATOR_GROUPS[groupName]
|
|
|
|
local position = getInvestigatorRowStartPos(investigatorCount, row)
|
|
|
|
for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do
|
|
|
|
spawnStarterDeck(investigatorName, INVESTIGATORS[investigatorName], position)
|
|
|
|
position:add(investigatorPositionShiftCol)
|
|
|
|
col = col + 1
|
|
|
|
if col > INVESTIGATOR_MAX_COLS then
|
|
|
|
col = 1
|
|
|
|
row = row + 1
|
|
|
|
position = getInvestigatorRowStartPos(investigatorCount, row)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawns the defined starter deck for the given investigator's.
|
|
|
|
---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData
|
|
|
|
function spawnStarterDeck(investigatorName, investigatorData, position)
|
|
|
|
for _, spawnSpec in ipairs(buildCommonSpawnSpec(investigatorName, investigatorData, position, true)) do
|
|
|
|
spawnBag.spawn(spawnSpec)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
local deckPos = Vector(position):add(investigatorSignatureOffset)
|
|
|
|
arkhamDb.getDecklist("None", investigatorData.starterDeck, true, false, false, function(slots)
|
|
|
|
local cardIdList = {}
|
|
|
|
for id, count in pairs(slots) do
|
|
|
|
for i = 1, count do
|
|
|
|
table.insert(cardIdList, id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = investigatorName .. "starter",
|
|
|
|
cards = cardIdList,
|
|
|
|
globalPos = self.positionToWorld(deckPos),
|
|
|
|
rotation = FACE_DOWN_ROTATION
|
|
|
|
})
|
|
|
|
end)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Clears the currently placed cards, then places cards for the given class and level spread
|
|
|
|
---@param cardClass string Class to place ("Guardian", "Seeker", etc)
|
|
|
|
---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.
|
|
|
|
function spawnClassCards(cardClass, isUpgraded)
|
|
|
|
prepareToPlaceCards()
|
|
|
|
Wait.frames(function() placeClassCards(cardClass, isUpgraded) end, 2)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawn the class cards.
|
|
|
|
---@param cardClass string Class to place ("Guardian", "Seeker", etc)
|
|
|
|
---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.
|
|
|
|
function placeClassCards(cardClass, isUpgraded)
|
|
|
|
if not allCardsBagApi.isIndexReady() then return end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded)
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local skillList = {}
|
|
|
|
local eventList = {}
|
|
|
|
local assetList = {}
|
|
|
|
for _, cardId in ipairs(cardIdList) do
|
|
|
|
local cardMetadata = allCardsBagApi.getCardById(cardId).metadata
|
|
|
|
if (cardMetadata.type == "Skill") then
|
|
|
|
table.insert(skillList, cardId)
|
|
|
|
elseif (cardMetadata.type == "Event") then
|
|
|
|
table.insert(eventList, cardId)
|
|
|
|
elseif (cardMetadata.type == "Asset") then
|
|
|
|
table.insert(assetList, cardId)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
local groupPos = Vector(startPositions.classCards)
|
|
|
|
if #skillList > 0 then
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = cardClass .. (isUpgraded and "upgraded" or "basic"),
|
|
|
|
cards = skillList,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
groupPos.z = groupPos.z + math.ceil(#skillList / 20) * cardRowOffset + cardGroupOffset
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
if #eventList > 0 then
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = cardClass .. "event" .. (isUpgraded and "upgraded" or "basic"),
|
|
|
|
cards = eventList,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
groupPos.z = groupPos.z + math.ceil(#eventList / 20) * cardRowOffset + cardGroupOffset
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
if #assetList > 0 then
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = cardClass .. "asset" .. (isUpgraded and "upgraded" or "basic"),
|
|
|
|
cards = assetList,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawns the investigator sets and all cards for the given cycle
|
|
|
|
---@param cycle string Name of a cycle, should match the standard used in card metadata
|
|
|
|
function spawnCycle(cycle)
|
|
|
|
if not allCardsBagApi.isIndexReady() then return end
|
|
|
|
|
|
|
|
prepareToPlaceCards()
|
|
|
|
spawnInvestigators(cycle)
|
|
|
|
|
|
|
|
-- sort custom cards
|
|
|
|
local sortByMetadata = false
|
|
|
|
if cycle == "Other" then
|
|
|
|
sortByMetadata = true
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
spawnBag.spawn({
|
|
|
|
name = "cycle" .. cycle,
|
|
|
|
cards = allCardsBagApi.getCardsByCycle(cycle, sortByMetadata),
|
|
|
|
globalPos = self.positionToWorld(startPositions.cycle),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
function spawnBonded()
|
|
|
|
prepareToPlaceCards()
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "bonded",
|
|
|
|
cards = BONDED_CARD_LIST,
|
|
|
|
globalPos = self.positionToWorld(startPositions.classCards),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
function spawnUpgradeSheets()
|
|
|
|
prepareToPlaceCards()
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "upgradeSheets",
|
|
|
|
cards = UPGRADE_SHEET_LIST,
|
|
|
|
globalPos = self.positionToWorld(startPositions.classCards),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "servitor",
|
|
|
|
cards = { "09080-m" },
|
|
|
|
globalPos = self.positionToWorld(startPositions.summonedServitor),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Clears the current cards, and places all basic weaknesses on the table.
|
|
|
|
function spawnWeaknesses()
|
|
|
|
if not allCardsBagApi.isIndexReady() then return end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
prepareToPlaceCards()
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local basicWeaknessList = {}
|
|
|
|
local otherWeaknessList = {}
|
|
|
|
for _, id in ipairs(allCardsBagApi.getUniqueWeaknesses()) do
|
|
|
|
local cardMetadata = allCardsBagApi.getCardById(id).metadata
|
|
|
|
if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then
|
|
|
|
table.insert(basicWeaknessList, id)
|
|
|
|
elseif excludedNonBasicWeaknesses[id] == nil then
|
|
|
|
table.insert(otherWeaknessList, id)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
local groupPos = Vector(startPositions.classCards)
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "basicWeaknesses",
|
|
|
|
cards = basicWeaknessList,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
groupPos.z = groupPos.z + math.ceil(#basicWeaknessList / 20) * cardRowOffset + cardGroupOffset
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "evolvedWeaknesses",
|
|
|
|
cards = EVOLVED_WEAKNESSES,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
groupPos.z = groupPos.z + math.ceil(#EVOLVED_WEAKNESSES / 20) * cardRowOffset + cardGroupOffset
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "otherWeaknesses",
|
|
|
|
cards = otherWeaknessList,
|
|
|
|
globalPos = self.positionToWorld(groupPos),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
spread = true,
|
|
|
|
spreadCols = 20
|
|
|
|
})
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
function spawnRandomWeakness(_, playerColor, isRightClick)
|
|
|
|
prepareToPlaceCards()
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
if not isRightClick then
|
|
|
|
local weaknessId = allCardsBagApi.getRandomWeaknessId()
|
|
|
|
if weaknessId == nil then
|
|
|
|
broadcastToAll("All basic weaknesses are in play!", { 0.9, 0.2, 0.2 })
|
|
|
|
else
|
|
|
|
spawnSingleWeakness(weaknessId)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
Player[playerColor].showInputDialog("Specify a trait for the weakness (split multiple eligible traits with '|'):", lastWeaknessTrait,
|
|
|
|
function(text)
|
|
|
|
lastWeaknessTrait = text
|
|
|
|
local availableWeaknesses = allCardsBagApi.buildAvailableWeaknesses(text)
|
|
|
|
if #availableWeaknesses > 0 then
|
|
|
|
spawnSingleWeakness(availableWeaknesses[math.random(#availableWeaknesses)])
|
|
|
|
else
|
|
|
|
broadcastToAll("No matching weakness available!", { 0.9, 0.2, 0.2 })
|
|
|
|
end
|
|
|
|
end)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- spawn the random weakness
|
|
|
|
function spawnSingleWeakness(weaknessId)
|
|
|
|
spawnBag.spawn({
|
|
|
|
name = "randomWeakness",
|
|
|
|
cards = { weaknessId },
|
|
|
|
globalPos = self.positionToWorld(startPositions.randomWeakness),
|
|
|
|
rotation = FACE_UP_ROTATION,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
do
|
|
|
|
local AllCardsBagApi = {}
|
|
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local function getAllCardsBag()
|
|
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag")
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- internal function to create a copy of the table to avoid operating on variables owned by different objects
|
|
|
|
local function returnCopyOfList(data)
|
|
|
|
local copiedList = {}
|
|
|
|
for _, id in ipairs(data) do
|
|
|
|
table.insert(copiedList, id)
|
|
|
|
end
|
|
|
|
return copiedList
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns a specific card from the bag, based on ArkhamDB ID
|
|
|
|
---@param id string ID of the card to retrieve
|
|
|
|
---@return table: If the indexes are still being constructed, returns an empty table.
|
|
|
|
-- Otherwise, a single table with the following fields
|
|
|
|
-- data: TTS object data, suitable for spawning the card
|
|
|
|
-- metadata: Table of parsed metadata
|
|
|
|
AllCardsBagApi.getCardById = function(id)
|
|
|
|
return getAllCardsBag().call("getCardById", { id = id })
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Gets a random basic weakness from the bag. Once a given ID has been returned it
|
|
|
|
-- will be removed from the list and cannot be selected again until a reload occurs
|
|
|
|
-- or the indexes are rebuilt, which will refresh the list to include all weaknesses.
|
|
|
|
---@return string: ID of the selected weakness
|
|
|
|
AllCardsBagApi.getRandomWeaknessId = function()
|
|
|
|
return getAllCardsBag().call("getRandomWeaknessId")
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
AllCardsBagApi.isIndexReady = function()
|
|
|
|
return getAllCardsBag().call("isIndexReady")
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Called by Hotfix bags when they load. If we are still loading indexes, then
|
|
|
|
-- the all cards and hotfix bags are being loaded together, and we can ignore
|
|
|
|
-- this call as the hotfix will be included in the initial indexing. If it is
|
|
|
|
-- called once indexing is complete it means the hotfix bag has been added
|
|
|
|
-- later, and we should rebuild the index to integrate the hotfix bag.
|
|
|
|
AllCardsBagApi.rebuildIndexForHotfix = function()
|
|
|
|
getAllCardsBag().call("rebuildIndexForHotfix")
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Searches the bag for cards which match the given name and returns a list.
|
|
|
|
-- Note that this is an O(n) search without index support. It may be slow.
|
|
|
|
---@param name string or string fragment to search for names
|
|
|
|
---@param exact boolean Whether the name match should be exact
|
|
|
|
AllCardsBagApi.getCardsByName = function(name, exact)
|
|
|
|
return returnCopyOfList(getAllCardsBag().call("getCardsByName", { name = name, exact = exact }))
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
AllCardsBagApi.isBagPresent = function()
|
|
|
|
return getAllCardsBag() and true
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns a list of cards from the bag matching a class and level (0 or upgraded)
|
|
|
|
---@param class string class to retrieve ("Guardian", "Seeker", etc)
|
|
|
|
---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0
|
|
|
|
---@return table: If the indexes are still being constructed, returns an empty table.
|
|
|
|
-- Otherwise, a list of tables, each with the following fields
|
|
|
|
-- data: TTS object data, suitable for spawning the card
|
|
|
|
-- metadata: Table of parsed metadata
|
|
|
|
AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)
|
|
|
|
return returnCopyOfList(getAllCardsBag().call("getCardsByClassAndLevel", { class = class, upgraded = upgraded }))
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns a list of cards from the bag matching a cycle
|
|
|
|
---@param cycle string Cycle to retrieve ("The Scarlet Keys" etc.)
|
|
|
|
---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID
|
|
|
|
---@return table: If the indexes are still being constructed, returns an empty table.
|
|
|
|
-- Otherwise, a list of tables, each with the following fields
|
|
|
|
-- data: TTS object data, suitable for spawning the card
|
|
|
|
-- metadata: Table of parsed metadata
|
|
|
|
AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)
|
|
|
|
return returnCopyOfList(getAllCardsBag().call("getCardsByCycle", { cycle = cycle, sortByMetadata = sortByMetadata }))
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Constructs a list of available basic weaknesses by starting with the full pool of basic
|
|
|
|
-- weaknesses then removing any which are currently in the play or deck construction areas
|
|
|
|
---@param traits? string Trait(s) to use as filter
|
|
|
|
---@return table: Array of weakness IDs which are valid to choose from
|
|
|
|
AllCardsBagApi.buildAvailableWeaknesses = function(traits)
|
|
|
|
return returnCopyOfList(getAllCardsBag().call("buildAvailableWeaknesses", traits))
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
AllCardsBagApi.getUniqueWeaknesses = function()
|
|
|
|
return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses"))
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
return AllCardsBagApi
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
end)
|
2024-07-27 21:47:52 -04:00
|
|
|
__bundle_register("playercards/PlayerCardPanelData", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
BONDED_CARD_LIST = {
|
|
|
|
"05314", -- Soothing Melody
|
|
|
|
"06277", -- Wish Eater
|
|
|
|
"06019", -- Bloodlust
|
|
|
|
"06022", -- Pendant of the Queen
|
|
|
|
"05317", -- Blood-rite
|
|
|
|
"06113", -- Essence of the Dream
|
|
|
|
"06028", -- Stars Are Right
|
|
|
|
"06025", -- Guardian of the Crystallizer
|
|
|
|
"06283", -- Unbound Beast
|
|
|
|
"06032", -- Zeal
|
|
|
|
"06031", -- Hope
|
|
|
|
"06033", -- Augur
|
|
|
|
"06331", -- Dream Parasite
|
|
|
|
"06015a", -- Dream-Gate
|
|
|
|
"10006", -- Aetheric Current (Yuggoth)
|
|
|
|
"10007", -- Aetheric Current (Yoth)
|
|
|
|
"10036", -- Blade of Yoth
|
|
|
|
"10039", -- Evanescent Ascension
|
|
|
|
"10045", -- Uncanny Growth
|
|
|
|
"10063", -- Bianca
|
|
|
|
"10086", -- Rot
|
|
|
|
"10087", -- Rot
|
|
|
|
"10088", -- Rot
|
|
|
|
"10089", -- Rot
|
|
|
|
"10090", -- Rot
|
|
|
|
"10106", -- Keeper of the Key
|
|
|
|
"10107", -- Servant of Brass
|
|
|
|
"10134", -- Twilight Diadem
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
UPGRADE_SHEET_LIST = {
|
|
|
|
"09040-c", -- Alchemical Distillation
|
|
|
|
"09023-c", -- Custom Modifications
|
|
|
|
"09059-c", -- Damning Testimony
|
|
|
|
"09041-c", -- Emperical Hypothesis
|
|
|
|
"09060-c", -- Friends in Low Places
|
|
|
|
"09101-c", -- Grizzled
|
|
|
|
"09061-c", -- Honed Instinct
|
|
|
|
"09021-c", -- Hunter's Armor
|
|
|
|
"09119-c", -- Hyperphysical Shotcaster
|
|
|
|
"09079-c", -- Living Ink
|
|
|
|
"09100-c", -- Makeshift Trap
|
|
|
|
"09099-c", -- Pocket Multi Tool
|
|
|
|
"09081-c", -- Power Word
|
|
|
|
"09081-t-c", -- Power Word (Taboo)
|
|
|
|
"09022-c", -- Runic Axe
|
|
|
|
"09022-t-c", -- Runic Axe (Taboo)
|
|
|
|
"09080-c", -- Summoned Servitor
|
|
|
|
"09042-c", -- Raven's Quill
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
EVOLVED_WEAKNESSES = {
|
|
|
|
"04039",
|
|
|
|
"04041",
|
|
|
|
"04042",
|
|
|
|
"53014",
|
|
|
|
"53015",
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
------------------ START INVESTIGATOR DATA DEFINITION ------------------
|
|
|
|
INVESTIGATOR_GROUPS = {
|
|
|
|
["Guardian"] = {
|
|
|
|
"Roland Banks",
|
|
|
|
"Zoey Samaras",
|
|
|
|
"Mark Harrigan",
|
|
|
|
"Leo Anderson",
|
|
|
|
"Carolyn Fern",
|
|
|
|
"Tommy Muldoon",
|
|
|
|
"Nathaniel Cho",
|
|
|
|
"Sister Mary",
|
|
|
|
"Daniela Reyes",
|
|
|
|
"Carson Sinclair",
|
|
|
|
"Wilson Richards"
|
|
|
|
},
|
|
|
|
["Seeker"] = {
|
|
|
|
"Daisy Walker",
|
|
|
|
"Rex Murphy",
|
|
|
|
"Minh Thi Phan",
|
|
|
|
"Ursula Downs",
|
|
|
|
"Joe Diamond",
|
|
|
|
"Mandy Thompson",
|
|
|
|
"Harvey Walters",
|
|
|
|
"Amanda Sharpe",
|
|
|
|
"Norman Withers",
|
|
|
|
"Vincent Lee",
|
|
|
|
"Kate Winthrop"
|
|
|
|
},
|
|
|
|
["Rogue"] = {
|
|
|
|
"\"Skids\" O'Toole",
|
|
|
|
"Jenny Barnes",
|
|
|
|
"Sefina Rousseau",
|
|
|
|
"Finn Edwards",
|
|
|
|
"Preston Fairmont",
|
|
|
|
"Tony Morgan",
|
|
|
|
"Winifred Habbamock",
|
|
|
|
"Trish Scarborough",
|
|
|
|
"Monterey Jack",
|
|
|
|
"Kymani Jones",
|
|
|
|
"Alessandra Zorzi"
|
|
|
|
},
|
|
|
|
["Mystic"] = {
|
|
|
|
"Agnes Baker",
|
|
|
|
"Jim Culver",
|
|
|
|
"Akachi Onyele",
|
|
|
|
"Father Mateo",
|
|
|
|
"Diana Stanley",
|
|
|
|
"Marie Lambeau",
|
|
|
|
"Luke Robinson",
|
|
|
|
"Jacqueline Fine",
|
|
|
|
"Dexter Drake",
|
|
|
|
"Lily Chen",
|
|
|
|
"Amina Zidane",
|
|
|
|
"Gloria Goldberg",
|
|
|
|
"Kōhaku Narukami"
|
|
|
|
},
|
|
|
|
["Survivor"] = {
|
|
|
|
"Wendy Adams",
|
|
|
|
"\"Ashcan\" Pete",
|
|
|
|
"William Yorick",
|
|
|
|
"Calvin Wright",
|
|
|
|
"Rita Young",
|
|
|
|
"Patrice Hathaway",
|
|
|
|
"Stella Clark",
|
|
|
|
"Silas Marsh",
|
|
|
|
"Bob Jenkins",
|
|
|
|
"Darrell Simmons",
|
|
|
|
"Hank Samson"
|
|
|
|
},
|
|
|
|
["Neutral"] = {
|
|
|
|
"Lola Hayes",
|
|
|
|
"Charlie Kane",
|
|
|
|
"Subject 5U-21"
|
|
|
|
},
|
|
|
|
["Core"] = {
|
|
|
|
"Roland Banks",
|
|
|
|
"Daisy Walker",
|
|
|
|
"\"Skids\" O'Toole",
|
|
|
|
"Agnes Baker",
|
|
|
|
"Wendy Adams"
|
|
|
|
},
|
|
|
|
["The Dunwich Legacy"] = {
|
|
|
|
"Zoey Samaras",
|
|
|
|
"Rex Murphy",
|
|
|
|
"Jenny Barnes",
|
|
|
|
"Jim Culver",
|
|
|
|
"\"Ashcan\" Pete"
|
|
|
|
},
|
|
|
|
["The Path to Carcosa"] = {
|
|
|
|
"Mark Harrigan",
|
|
|
|
"Minh Thi Phan",
|
|
|
|
"Sefina Rousseau",
|
|
|
|
"Akachi Onyele",
|
|
|
|
"William Yorick",
|
|
|
|
"Lola Hayes"
|
|
|
|
},
|
|
|
|
["The Forgotten Age"] = {
|
|
|
|
"Leo Anderson",
|
|
|
|
"Ursula Downs",
|
|
|
|
"Finn Edwards",
|
|
|
|
"Father Mateo",
|
|
|
|
"Calvin Wright"
|
|
|
|
},
|
|
|
|
["The Circle Undone"] = {
|
|
|
|
"Carolyn Fern",
|
|
|
|
"Joe Diamond",
|
|
|
|
"Preston Fairmont",
|
|
|
|
"Diana Stanley",
|
|
|
|
"Rita Young",
|
|
|
|
"Marie Lambeau"
|
|
|
|
},
|
|
|
|
["The Dream-Eaters"] = {
|
|
|
|
"Tommy Muldoon",
|
|
|
|
"Mandy Thompson",
|
|
|
|
"Tony Morgan",
|
|
|
|
"Luke Robinson",
|
|
|
|
"Patrice Hathaway"
|
|
|
|
},
|
|
|
|
["Investigator Packs"] = {
|
|
|
|
"Nathaniel Cho",
|
|
|
|
"Harvey Walters",
|
|
|
|
"Winifred Habbamock",
|
|
|
|
"Jacqueline Fine",
|
|
|
|
"Stella Clark",
|
|
|
|
"Gloria Goldberg"
|
|
|
|
},
|
|
|
|
["The Innsmouth Conspiracy"] = {
|
|
|
|
"Sister Mary",
|
|
|
|
"Amanda Sharpe",
|
|
|
|
"Trish Scarborough",
|
|
|
|
"Dexter Drake",
|
|
|
|
"Silas Marsh"
|
|
|
|
},
|
|
|
|
["Edge of the Earth"] = {
|
|
|
|
"Daniela Reyes",
|
|
|
|
"Norman Withers",
|
|
|
|
"Monterey Jack",
|
|
|
|
"Lily Chen",
|
|
|
|
"Bob Jenkins"
|
|
|
|
},
|
|
|
|
["The Scarlet Keys"] = {
|
|
|
|
"Carson Sinclair",
|
|
|
|
"Vincent Lee",
|
|
|
|
"Kymani Jones",
|
|
|
|
"Amina Zidane",
|
|
|
|
"Darrell Simmons",
|
|
|
|
"Charlie Kane"
|
|
|
|
},
|
|
|
|
["The Feast of Hemlock Vale"] = {
|
|
|
|
"Wilson Richards",
|
|
|
|
"Kate Winthrop",
|
|
|
|
"Alessandra Zorzi",
|
|
|
|
"Kōhaku Narukami",
|
|
|
|
"Hank Samson"
|
|
|
|
}
|
|
|
|
}
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
INVESTIGATORS = {}
|
|
|
|
-- Core Box
|
|
|
|
INVESTIGATORS["Roland Banks"] = {
|
|
|
|
cards = { "01001", "01001-p", "01001-pf", "01001-pb" },
|
|
|
|
minicards = { "01001-m" },
|
|
|
|
signatures = { "01006", "01007", "90030", "90031", "90025", "90026", "90027", "90028", "90029", "98005", "98006" },
|
|
|
|
starterDeck = "2624931"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Daisy Walker"] = {
|
|
|
|
cards = { "01002", "01002-p", "01002-pf", "01002-pb" },
|
|
|
|
minicards = { "01002-m" },
|
|
|
|
signatures = { "01008", "01009", "90002", "90003" },
|
|
|
|
starterDeck = "2624938"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["\"Skids\" O'Toole"] = {
|
|
|
|
cards = { "01003", "01003-p", "01003-pf", "01003-pb" },
|
|
|
|
minicards = { "01003-m" },
|
|
|
|
signatures = { "01010", "01011", "90009", "90010" },
|
|
|
|
starterDeck = "2624940"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Agnes Baker"] = {
|
|
|
|
cards = { "01004", "01004-p", "01004-pf", "01004-pb" },
|
|
|
|
minicards = { "01004-m" },
|
|
|
|
signatures = { "01012", "01013", "90018", "90019" },
|
|
|
|
starterDeck = "2624944"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Wendy Adams"] = {
|
|
|
|
cards = { "01005", "01005-p", "01005-pf", "01005-pb" },
|
|
|
|
minicards = { "01005-m" },
|
|
|
|
signatures = { "01014", "01015", "01515", "90039", "90040", "90038" },
|
|
|
|
starterDeck = "2624949"
|
|
|
|
}
|
|
|
|
-- The Dunwich Legacy
|
|
|
|
INVESTIGATORS["Zoey Samaras"] = {
|
|
|
|
cards = { "02001", "02001-p", "02001-pf", "02001-pb" },
|
|
|
|
minicards = { "02001-m" },
|
|
|
|
signatures = { "02006", "02007", "90060", "90061" },
|
|
|
|
starterDeck = "2624950"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Rex Murphy"] = {
|
|
|
|
cards = { "02002", "02002-t", "02002-p", "02002-pf", "02002-pb" },
|
|
|
|
minicards = { "02002-m" },
|
|
|
|
signatures = { "02008", "02009", "90079", "90080" },
|
|
|
|
starterDeck = "2624958"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Jenny Barnes"] = {
|
|
|
|
cards = { "02003" },
|
|
|
|
minicards = { "02003-m" },
|
|
|
|
signatures = { "02010", "02011", "98002", "98003" },
|
|
|
|
starterDeck = "2624961"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Jim Culver"] = {
|
|
|
|
cards = { "02004", "02004-p", "02004-pf", "02004-pb" },
|
|
|
|
minicards = { "02004-m" },
|
|
|
|
signatures = { "02012", "02013", "90050", "90051", "90052", "90053" },
|
|
|
|
starterDeck = "2624965"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["\"Ashcan\" Pete"] = {
|
|
|
|
cards = { "02005", "02005-p", "02005-pf", "02005-pb" },
|
|
|
|
minicards = { "02005-m" },
|
|
|
|
signatures = { "02014", "02015", "90047", "90048" },
|
|
|
|
starterDeck = "2624969"
|
|
|
|
}
|
|
|
|
-- The Path to Carcosa
|
|
|
|
INVESTIGATORS["Mark Harrigan"] = {
|
|
|
|
cards = { "03001" },
|
|
|
|
minicards = { "03001-m" },
|
|
|
|
signatures = { "03007", "03008", "03009" },
|
|
|
|
starterDeck = "2624975"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Minh Thi Phan"] = {
|
|
|
|
cards = { "03002" },
|
|
|
|
minicards = { "03002-m" },
|
|
|
|
signatures = { "03010", "03011" },
|
|
|
|
starterDeck = "2624979"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Sefina Rousseau"] = {
|
|
|
|
cards = { "03003" },
|
|
|
|
minicards = { "03003-m" },
|
|
|
|
signatures = { "03012", "03012", "03012", "03013" },
|
|
|
|
starterDeck = "2624981"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Akachi Onyele"] = {
|
|
|
|
cards = { "03004" },
|
|
|
|
minicards = { "03004-m" },
|
|
|
|
signatures = { "03014", "03015" },
|
|
|
|
starterDeck = "2624984"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["William Yorick"] = {
|
|
|
|
cards = { "03005" },
|
|
|
|
minicards = { "03005-m" },
|
|
|
|
signatures = { "03016", "03017" },
|
|
|
|
starterDeck = "2624988"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Lola Hayes"] = {
|
|
|
|
cards = { "03006", "03006-t" },
|
|
|
|
minicards = { "03006-m" },
|
|
|
|
signatures = { "03018", "03018", "03019", "03019", "03019-t", "03019-t" },
|
|
|
|
starterDeck = "2624990"
|
|
|
|
}
|
|
|
|
-- The Forgotten Age
|
|
|
|
INVESTIGATORS["Leo Anderson"] = {
|
|
|
|
cards = { "04001" },
|
|
|
|
minicards = { "04001-m" },
|
|
|
|
signatures = { "04006", "04007" },
|
|
|
|
starterDeck = "2624994"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Ursula Downs"] = {
|
|
|
|
cards = { "04002" },
|
|
|
|
minicards = { "04002-m" },
|
|
|
|
signatures = { "04008", "04009" },
|
|
|
|
starterDeck = "2625000"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Finn Edwards"] = {
|
|
|
|
cards = { "04003" },
|
|
|
|
minicards = { "04003-m" },
|
|
|
|
signatures = { "04010", "04011", "04012" },
|
|
|
|
starterDeck = "2625003"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Father Mateo"] = {
|
|
|
|
cards = { "04004" },
|
|
|
|
minicards = { "04004-m" },
|
|
|
|
signatures = { "04013", "04014" },
|
|
|
|
starterDeck = "2625005"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Calvin Wright"] = {
|
|
|
|
cards = { "04005" },
|
|
|
|
minicards = { "04005-m" },
|
|
|
|
signatures = { "04015", "04016" },
|
|
|
|
starterDeck = "2625007"
|
|
|
|
}
|
|
|
|
-- The Circle Undone
|
|
|
|
INVESTIGATORS["Carolyn Fern"] = {
|
|
|
|
cards = { "05001" },
|
|
|
|
minicards = { "05001-m" },
|
|
|
|
signatures = { "05007", "05008", "98011", "98012" },
|
|
|
|
starterDeck = "2626342"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Joe Diamond"] = {
|
|
|
|
cards = { "05002" },
|
|
|
|
minicards = { "05002-m" },
|
|
|
|
signatures = { "05009", "05010" },
|
|
|
|
starterDeck = "2626347"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Preston Fairmont"] = {
|
|
|
|
cards = { "05003" },
|
|
|
|
minicards = { "05003-m" },
|
|
|
|
signatures = { "05011", "05012" },
|
|
|
|
starterDeck = "2626365"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Diana Stanley"] = {
|
|
|
|
cards = { "05004" },
|
|
|
|
minicards = { "05004-m" },
|
|
|
|
signatures = { "05013", "05014", "05015" },
|
|
|
|
starterDeck = "2626385"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Rita Young"] = {
|
|
|
|
cards = { "05005" },
|
|
|
|
minicards = { "05005-m" },
|
|
|
|
signatures = { "05016", "05017" },
|
|
|
|
starterDeck = "2626387"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Marie Lambeau"] = {
|
|
|
|
cards = { "05006" },
|
|
|
|
minicards = { "05006-m" },
|
|
|
|
signatures = { "05018", "05019" },
|
|
|
|
starterDeck = "2626395"
|
|
|
|
}
|
|
|
|
-- The Dream-Eaters
|
|
|
|
INVESTIGATORS["Tommy Muldoon"] = {
|
|
|
|
cards = { "06001" },
|
|
|
|
minicards = { "06001-m" },
|
|
|
|
signatures = { "06006", "06007" },
|
|
|
|
starterDeck = "2626402"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Mandy Thompson"] = {
|
|
|
|
cards = { "06002", "06002-t" },
|
|
|
|
minicards = { "06002-m" },
|
|
|
|
signatures = { "06008", "06008", "06008", "06009" },
|
|
|
|
starterDeck = "2626410"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Tony Morgan"] = {
|
|
|
|
cards = { "06003" },
|
|
|
|
minicards = { "06003-m" },
|
|
|
|
signatures = { "06010", "06011", "06011", "06012" },
|
|
|
|
starterDeck = "2626446"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Luke Robinson"] = {
|
|
|
|
cards = { "06004" },
|
|
|
|
minicards = { "06004-m" },
|
|
|
|
signatures = { "06013", "06014", "06015" },
|
|
|
|
starterDeck = "2626452"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Patrice Hathaway"] = {
|
|
|
|
cards = { "06005" },
|
|
|
|
minicards = { "06005-m" },
|
|
|
|
signatures = { "06016", "06017" },
|
|
|
|
starterDeck = "2626461"
|
|
|
|
}
|
|
|
|
-- Starter Decks
|
|
|
|
INVESTIGATORS["Nathaniel Cho"] = {
|
|
|
|
cards = { "60101" },
|
|
|
|
minicards = { "60101-m" },
|
|
|
|
signatures = { "60102", "60103" },
|
|
|
|
starterDeck = "2643925"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Harvey Walters"] = {
|
|
|
|
cards = { "60201" },
|
|
|
|
minicards = { "60201-m" },
|
|
|
|
signatures = { "60202", "60203" },
|
|
|
|
starterDeck = "2643928"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Winifred Habbamock"] = {
|
|
|
|
cards = { "60301" },
|
|
|
|
minicards = { "60301-m" },
|
|
|
|
signatures = { "60302", "60303" },
|
|
|
|
starterDeck = "2643931"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Jacqueline Fine"] = {
|
|
|
|
cards = { "60401" },
|
|
|
|
minicards = { "60401-m" },
|
|
|
|
signatures = { "60402", "60403" },
|
|
|
|
starterDeck = "2643932"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Stella Clark"] = {
|
|
|
|
cards = { "60501" },
|
|
|
|
minicards = { "60501-m" },
|
|
|
|
signatures = { "60502", "60502", "60502", "60503" },
|
|
|
|
starterDeck = "2643934"
|
|
|
|
}
|
|
|
|
-- The Innsmouth Conspiracy
|
|
|
|
INVESTIGATORS["Sister Mary"] = {
|
|
|
|
cards = { "07001" },
|
|
|
|
minicards = { "07001-m" },
|
|
|
|
signatures = { "07006", "07007" },
|
|
|
|
starterDeck = "2626464"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Amanda Sharpe"] = {
|
|
|
|
cards = { "07002" },
|
|
|
|
minicards = { "07002-m" },
|
|
|
|
signatures = { "07008", "07009" },
|
|
|
|
starterDeck = "2626469"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Trish Scarborough"] = {
|
|
|
|
cards = { "07003", "07003-t" },
|
|
|
|
minicards = { "07003-m" },
|
|
|
|
signatures = { "07010", "07011" },
|
|
|
|
starterDeck = "2626479"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Dexter Drake"] = {
|
|
|
|
cards = { "07004" },
|
|
|
|
minicards = { "07004-m" },
|
|
|
|
signatures = { "07012", "07013", "98017", "98018" },
|
|
|
|
starterDeck = "2626672"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Silas Marsh"] = {
|
|
|
|
cards = { "07005" },
|
|
|
|
minicards = { "07005-m" },
|
|
|
|
signatures = { "07014", "07015", "07016", "98014", "98015" },
|
|
|
|
starterDeck = "2626685"
|
|
|
|
}
|
|
|
|
-- Edge of the Earth
|
|
|
|
INVESTIGATORS["Daniela Reyes"] = {
|
|
|
|
cards = { "08001" },
|
|
|
|
minicards = { "08001-m" },
|
|
|
|
signatures = { "08002", "08003" },
|
|
|
|
starterDeck = "2634588"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Norman Withers"] = {
|
|
|
|
cards = { "08004" },
|
|
|
|
minicards = { "08004-m" },
|
|
|
|
signatures = { "08005", "08006", "98008", "98009" },
|
|
|
|
starterDeck = "2634603"
|
2024-06-09 10:10:21 -04:00
|
|
|
}
|
2024-07-27 21:47:52 -04:00
|
|
|
INVESTIGATORS["Monterey Jack"] = {
|
|
|
|
cards = { "08007", "08007-p", "08007-pf", "08007-pb" },
|
|
|
|
minicards = { "08007-m" },
|
|
|
|
signatures = { "08008", "08009", "90063", "90064" },
|
|
|
|
starterDeck = "2634652"
|
2024-03-10 09:56:22 -04:00
|
|
|
}
|
2024-07-27 21:47:52 -04:00
|
|
|
INVESTIGATORS["Lily Chen"] = {
|
|
|
|
cards = { "08010" },
|
|
|
|
minicards = { "08010-m" },
|
|
|
|
signatures = { "08011a", "08012a", "08013a", "08014a", "08015", "08015", "08015", "08015" },
|
|
|
|
starterDeck = "2634658"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Bob Jenkins"] = {
|
|
|
|
cards = { "08016" },
|
|
|
|
minicards = { "08016-m" },
|
|
|
|
signatures = { "08017", "08018" },
|
|
|
|
starterDeck = "2634643"
|
|
|
|
}
|
|
|
|
-- The Scarlet Keys
|
|
|
|
INVESTIGATORS["Carson Sinclair"] = {
|
|
|
|
cards = { "09001" },
|
|
|
|
minicards = { "09001-m" },
|
|
|
|
signatures = { "09002", "09002", "09003" },
|
|
|
|
starterDeck = "2634667"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Vincent Lee"] = {
|
|
|
|
cards = { "09004" },
|
|
|
|
minicards = { "09004-m" },
|
|
|
|
signatures = { "09005", "09006", "09006", "09006", "09006", "09007" },
|
|
|
|
starterDeck = "2634675"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Kymani Jones"] = {
|
|
|
|
cards = { "09008" },
|
|
|
|
minicards = { "09008-m" },
|
|
|
|
signatures = { "09009", "09010" },
|
|
|
|
starterDeck = "2634701"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Amina Zidane"] = {
|
|
|
|
cards = { "09011" },
|
|
|
|
minicards = { "09011-m" },
|
|
|
|
signatures = { "09012", "09013", "09014" },
|
|
|
|
starterDeck = "2634697"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Darrell Simmons"] = {
|
|
|
|
cards = { "09015" },
|
|
|
|
minicards = { "09015-m" },
|
|
|
|
signatures = { "09016", "09017" },
|
|
|
|
starterDeck = "2634757"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Charlie Kane"] = {
|
|
|
|
cards = { "09018" },
|
|
|
|
minicards = { "09018-m" },
|
|
|
|
signatures = { "09019", "09020" },
|
|
|
|
starterDeck = "2634706"
|
|
|
|
}
|
|
|
|
-- The Feast of Hemlock Vale
|
|
|
|
INVESTIGATORS["Wilson Richards"] = {
|
|
|
|
cards = { "10001" },
|
|
|
|
minicards = { "10001-m" },
|
|
|
|
signatures = { "10002", "10003" },
|
|
|
|
starterDeck = "3893753"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Kate Winthrop"] = {
|
|
|
|
cards = { "10004" },
|
|
|
|
minicards = { "10004-m" },
|
|
|
|
signatures = { "10005", "10006", "10007", "10008" },
|
|
|
|
starterDeck = "3893779"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Alessandra Zorzi"] = {
|
|
|
|
cards = { "10009" },
|
|
|
|
minicards = { "10009-m" },
|
|
|
|
signatures = { "10010", "10010", "10010", "10011" },
|
|
|
|
starterDeck = "3893775"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Kōhaku Narukami"] = {
|
|
|
|
cards = { "10012" },
|
|
|
|
minicards = { "10012-m" },
|
|
|
|
signatures = { "10013", "10014" },
|
|
|
|
starterDeck = "3893763"
|
|
|
|
}
|
|
|
|
INVESTIGATORS["Hank Samson"] = {
|
|
|
|
cards = { "10015", "10015-b1", "10015-b2" },
|
|
|
|
minicards = { "10015-m" },
|
|
|
|
signatures = { "10017", "10018" },
|
|
|
|
starterDeck = "3893788"
|
|
|
|
}
|
|
|
|
-- PnP content
|
|
|
|
INVESTIGATORS["Subject 5U-21"] = {
|
|
|
|
cards = { "89001" },
|
|
|
|
minicards = { "89001-m" },
|
|
|
|
signatures = { "89002", "89003", "89003", "89003", "89004", "89004", "89004", "89005" },
|
|
|
|
starterDeck = "3893795"
|
|
|
|
}
|
|
|
|
-- Promo content
|
|
|
|
INVESTIGATORS["Gloria Goldberg"] = {
|
|
|
|
cards = { "98019" },
|
|
|
|
minicards = { "98019-m" },
|
|
|
|
signatures = { "98020", "98021" },
|
|
|
|
starterDeck = "2636199"
|
|
|
|
}
|
|
|
|
------------------ END INVESTIGATOR DATA DEFINITION ------------------
|
|
|
|
end)
|
|
|
|
__bundle_register("playercards/SpawnBag", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
require("playercards/PlayerCardSpawner")
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Allows spawning of defined lists of cards which will be created from the template in the All
|
|
|
|
-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while
|
|
|
|
-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the
|
|
|
|
-- spawned objects. Objects moved out of this area will not be cleaned up.
|
|
|
|
--
|
|
|
|
-- SpawnSpec: Spawning requires a spawn specification with the following structure:
|
|
|
|
-- {
|
|
|
|
-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned,
|
|
|
|
-- but each requires a separate name
|
|
|
|
-- cards: A list of card IDs to be spawned
|
|
|
|
-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be
|
|
|
|
-- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 }
|
|
|
|
-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with
|
|
|
|
-- globalPos, this should be a valid Vector with x, y, and z defined
|
|
|
|
-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a
|
|
|
|
-- spread moving to the right. globalPos will define the location of the first card, each
|
|
|
|
-- after that will be moved a predefined distance
|
|
|
|
-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be
|
|
|
|
-- laid out in before starting a new row. If spread is true but spreadCols is not set, all
|
|
|
|
-- cards will be in a single row (however long that may be)
|
|
|
|
-- }
|
|
|
|
-- See BondedBag.ttslua for an example
|
|
|
|
do
|
|
|
|
local allCardsBagApi = require("playercards/AllCardsBagApi")
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local SpawnBag = {}
|
|
|
|
local internal = {}
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- To assist debugging, will draw a box around the recall zone when it's set up
|
|
|
|
local SHOW_RECALL_ZONE = false
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Distance to expand the recall zone around any added object.
|
|
|
|
local RECALL_BUFFER_X = 0.9
|
|
|
|
local RECALL_BUFFER_Z = 0.5
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- In order to mimic the behavior of the previous memory buttons we use a temporary bag when
|
|
|
|
-- recalling objects. This bag is tiny and transparent, and will be placed at the same location as
|
|
|
|
-- this object. Once all placed cards are recalled bag to this bag, it will be destroyed
|
|
|
|
local RECALL_BAG = {
|
|
|
|
Name = "Bag",
|
|
|
|
Transform = {
|
|
|
|
scaleX = 0.01,
|
|
|
|
scaleY = 0.01,
|
|
|
|
scaleZ = 0.01,
|
|
|
|
},
|
|
|
|
ColorDiffuse = {
|
|
|
|
r = 0,
|
|
|
|
g = 0,
|
|
|
|
b = 0,
|
|
|
|
a = 0,
|
|
|
|
},
|
|
|
|
Locked = true,
|
|
|
|
Grid = true,
|
|
|
|
Snap = false,
|
|
|
|
Tooltip = false
|
2024-06-09 10:10:21 -04:00
|
|
|
}
|
2024-07-27 21:47:52 -04:00
|
|
|
|
|
|
|
-- Tracks what has been placed by this "bag" so they can be recalled
|
|
|
|
local placedSpecs = {}
|
|
|
|
local placedObjectGuids = {}
|
|
|
|
local recallZone = nil
|
|
|
|
|
|
|
|
-- Loads a table of saved state, extracted during the parent object's onLoad
|
|
|
|
SpawnBag.loadFromSave = function(saveTable)
|
|
|
|
placedSpecs = saveTable.placed
|
|
|
|
placedObjectGuids = saveTable.placedObjects
|
|
|
|
recallZone = saveTable.recall
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Generates a table of save state that can be included in the parent object's onSave
|
|
|
|
SpawnBag.getStateForSave = function()
|
|
|
|
return {
|
|
|
|
placed = placedSpecs,
|
|
|
|
placedObjects = placedObjectGuids,
|
|
|
|
recall = recallZone,
|
|
|
|
}
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples
|
|
|
|
SpawnBag.spawn = function(spawnSpec)
|
|
|
|
-- Limit to one placement at a time
|
|
|
|
if placedSpecs[spawnSpec.name] or spawnSpec == nil then return end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local cardsToSpawn = {}
|
|
|
|
for _, cardId in ipairs(spawnSpec.cards) do
|
|
|
|
local card = allCardsBagApi.getCardById(cardId)
|
|
|
|
if card ~= nil then
|
|
|
|
table.insert(cardsToSpawn, card)
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
|
|
|
if spawnSpec.spread then
|
|
|
|
Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject)
|
2024-06-09 10:10:21 -04:00
|
|
|
else
|
2024-07-27 21:47:52 -04:00
|
|
|
-- TTS decks come out in reverse order of the cards, reverse the list so the input order stays
|
|
|
|
-- This only applies for decks; spreads are spawned by us in the order given
|
|
|
|
if spawnSpec.rotation.z ~= 180 then
|
|
|
|
cardsToSpawn = internal.reverseList(cardsToSpawn)
|
|
|
|
end
|
|
|
|
Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, internal.recordPlacedObject)
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
placedSpecs[spawnSpec.name] = true
|
2024-03-10 09:56:22 -04:00
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Recalls all spawned objects to the bag, and clears the placedObjectGuids list
|
|
|
|
---@param fast boolean If true, cards will be deleted directly without faking the bag recall.
|
|
|
|
SpawnBag.recall = function(fast)
|
|
|
|
if fast then
|
|
|
|
internal.deleteSpawned()
|
|
|
|
else
|
|
|
|
internal.recallSpawned()
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- We've recalled everything we can, some cards may have been moved out of the card area. Just reset at this point.
|
|
|
|
placedSpecs = {}
|
|
|
|
placedObjectGuids = {}
|
|
|
|
recallZone = nil
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Delete all spawned cards
|
|
|
|
internal.deleteSpawned = function()
|
|
|
|
for guid, _ in pairs(placedObjectGuids) do
|
|
|
|
local obj = getObjectFromGUID(guid)
|
|
|
|
if (obj ~= nil) then
|
|
|
|
if (internal.isInRecallZone(obj)) then
|
|
|
|
obj.destruct()
|
|
|
|
end
|
|
|
|
placedObjectGuids[guid] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Recalls spawned cards with a fake bag that replicates the memory bag recall style
|
|
|
|
internal.recallSpawned = function()
|
|
|
|
local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() })
|
|
|
|
for guid, _ in pairs(placedObjectGuids) do
|
|
|
|
local obj = getObjectFromGUID(guid)
|
|
|
|
if (obj ~= nil) then
|
|
|
|
if (internal.isInRecallZone(obj)) then
|
|
|
|
trash.putObject(obj)
|
|
|
|
end
|
|
|
|
placedObjectGuids[guid] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
trash.destruct()
|
2023-01-29 19:31:52 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Callback for when an object has been spawned. Tracks the object for later recall and updates the recall zone.
|
|
|
|
internal.recordPlacedObject = function(spawned)
|
|
|
|
placedObjectGuids[spawned.getGUID()] = true
|
|
|
|
internal.expandRecallZone(spawned)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Expands the current recall zone based on the position of the given object. The recall zone will
|
|
|
|
-- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer
|
|
|
|
internal.expandRecallZone = function(spawnedCard)
|
|
|
|
local pos = spawnedCard.getPosition()
|
|
|
|
if (recallZone == nil) then
|
|
|
|
-- First card out of the bag, initialize surrounding that
|
|
|
|
recallZone = {}
|
|
|
|
recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z }
|
|
|
|
recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z }
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
if pos.x > recallZone.upperLeft.x then
|
|
|
|
recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X
|
|
|
|
end
|
|
|
|
if pos.x < recallZone.lowerRight.x then
|
|
|
|
recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X
|
|
|
|
end
|
|
|
|
if pos.z > recallZone.upperLeft.z then
|
|
|
|
recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z
|
|
|
|
end
|
|
|
|
if pos.z < recallZone.lowerRight.z then
|
|
|
|
recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z
|
|
|
|
end
|
|
|
|
|
|
|
|
if SHOW_RECALL_ZONE then
|
|
|
|
local y = 1.5
|
|
|
|
local thick = 0.05
|
|
|
|
Global.setVectorLines({
|
|
|
|
{
|
|
|
|
points = { { recallZone.upperLeft.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.lowerRight.z } },
|
|
|
|
color = { 1, 0, 0 },
|
|
|
|
thickness = thick,
|
|
|
|
rotation = { 0, 0, 0 }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
points = { { recallZone.upperLeft.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.lowerRight.z } },
|
|
|
|
color = { 1, 0, 0 },
|
|
|
|
thickness = thick,
|
|
|
|
rotation = { 0, 0, 0 }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
points = { { recallZone.lowerRight.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.upperLeft.z } },
|
|
|
|
color = { 1, 0, 0 },
|
|
|
|
thickness = thick,
|
|
|
|
rotation = { 0, 0, 0 }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
points = { { recallZone.lowerRight.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.upperLeft.z } },
|
|
|
|
color = { 1, 0, 0 },
|
|
|
|
thickness = thick,
|
|
|
|
rotation = { 0, 0, 0 }
|
|
|
|
}
|
|
|
|
})
|
2024-03-10 09:56:22 -04:00
|
|
|
end
|
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Checks to see if the given object is in the current recall zone. If there isn't a recall zone,
|
|
|
|
-- will return true so that everything can be easily cleaned up.
|
|
|
|
internal.isInRecallZone = function(obj)
|
|
|
|
if (recallZone == nil) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
local pos = obj.getPosition()
|
|
|
|
return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x
|
|
|
|
and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z)
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
internal.reverseList = function(list)
|
|
|
|
local reversed = {}
|
|
|
|
for i = 1, #list do
|
|
|
|
reversed[i] = list[#list - i + 1]
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
return reversed
|
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
return SpawnBag
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end)
|
|
|
|
__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
do
|
|
|
|
local GUIDReferenceApi = {}
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local function getGuidHandler()
|
|
|
|
return getObjectFromGUID("123456")
|
2024-02-04 10:51:51 -05:00
|
|
|
end
|
2024-03-10 09:56:22 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns the matching object
|
|
|
|
---@param owner string Parent object for this search
|
|
|
|
---@param type string Type of object to search for
|
|
|
|
---@return any: Object reference to the matching object
|
|
|
|
GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)
|
|
|
|
return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type })
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns all matching objects as a table with references
|
|
|
|
---@param type string Type of object to search for
|
|
|
|
---@return table: List of object references to matching objects
|
|
|
|
GUIDReferenceApi.getObjectsByType = function(type)
|
|
|
|
return getGuidHandler().call("getObjectsByType", type)
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns all matching objects as a table with references
|
|
|
|
---@param owner string Parent object for this search
|
|
|
|
---@return table: List of object references to matching objects
|
|
|
|
GUIDReferenceApi.getObjectsByOwner = function(owner)
|
|
|
|
return getGuidHandler().call("getObjectsByOwner", owner)
|
2024-01-06 21:32:29 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Sends new information to the reference handler to edit the main index
|
|
|
|
---@param owner string Parent of the object
|
|
|
|
---@param type string Type of the object
|
|
|
|
---@param guid string GUID of the object
|
|
|
|
GUIDReferenceApi.editIndex = function(owner, type, guid)
|
|
|
|
return getGuidHandler().call("editIndex", {
|
|
|
|
owner = owner,
|
|
|
|
type = type,
|
|
|
|
guid = guid
|
|
|
|
})
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns the owner of an object or the object it's located on
|
|
|
|
---@param object tts__GameObject Object for this search
|
|
|
|
---@return string: Parent of the object or object it's located on
|
|
|
|
GUIDReferenceApi.getOwnerOfObject = function(object)
|
|
|
|
return getGuidHandler().call("getOwnerOfObject", object)
|
2024-01-06 21:32:29 -05:00
|
|
|
end
|
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
return GUIDReferenceApi
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end)
|
|
|
|
__bundle_register("playercards/PlayerCardSpawner", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
-- Amount to shift for the next card (zShift) or next row of cards (xShift)
|
|
|
|
-- Note that the table rotation is weird, and the X axis is vertical while the
|
|
|
|
-- Z axis is horizontal
|
|
|
|
local SPREAD_Z_SHIFT = -2.3
|
|
|
|
local SPREAD_X_SHIFT = -3.66
|
2023-08-27 21:09:46 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
Spawner = { }
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawns a list of cards at the given position/rotation. This will separate cards by size -
|
|
|
|
-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If
|
|
|
|
-- there are different types, the provided callback will be called once for each type as it spawns
|
|
|
|
-- either a card or deck.
|
|
|
|
---@param cardList table A list of Player Card data structures (data/metadata)
|
|
|
|
---@param pos tts__Vector table where the cards should be spawned (global)
|
|
|
|
---@param rot tts__Vector table for the orientation of the spawned cards (global)
|
|
|
|
---@param sort boolean True if this list of cards should be sorted before spawning
|
|
|
|
---@param callback? function Callback to be called after the card/deck spawns.
|
|
|
|
Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
|
|
|
|
if sort then
|
|
|
|
table.sort(cardList, Spawner.cardComparator)
|
|
|
|
end
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local miniCards = { }
|
|
|
|
local standardCards = { }
|
|
|
|
local investigatorCards = { }
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
for _, card in ipairs(cardList) do
|
|
|
|
if card.metadata.type == "Investigator" then
|
|
|
|
table.insert(investigatorCards, card)
|
|
|
|
elseif card.metadata.type == "Minicard" then
|
|
|
|
-- set proper scale for minicards
|
|
|
|
card.data.Transform.scaleX = 0.6
|
|
|
|
card.data.Transform.scaleZ = 0.6
|
|
|
|
table.insert(miniCards, card)
|
|
|
|
else
|
|
|
|
table.insert(standardCards, card)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
end
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck
|
|
|
|
local position = { x = pos.x, y = pos.y, z = pos.z }
|
|
|
|
Spawner.spawn(investigatorCards, position, rot, callback)
|
|
|
|
|
|
|
|
position.y = position.y + (#investigatorCards + #standardCards) * 0.07
|
|
|
|
Spawner.spawn(standardCards, position, rot, callback)
|
|
|
|
|
|
|
|
position.y = position.y + (#standardCards + #miniCards) * 0.07
|
|
|
|
Spawner.spawn(miniCards, position, rot, callback)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)
|
|
|
|
if sort then
|
|
|
|
table.sort(cardList, Spawner.cardComparator)
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
local position = { x = startPos.x, y = startPos.y, z = startPos.z }
|
|
|
|
-- Special handle the first row if we have less than a full single row, but only if there's a
|
|
|
|
-- reasonable max column count. Single-row spreads will send a large value for maxCols
|
|
|
|
if maxCols < 100 and #cardList < maxCols then
|
|
|
|
position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
local cardsInRow = 0
|
|
|
|
local rows = 0
|
|
|
|
for _, card in ipairs(cardList) do
|
|
|
|
Spawner.spawn({ card }, position, rot, callback)
|
|
|
|
position.z = position.z + SPREAD_Z_SHIFT
|
|
|
|
cardsInRow = cardsInRow + 1
|
|
|
|
if cardsInRow >= maxCols then
|
|
|
|
rows = rows + 1
|
|
|
|
local cardsForRow = #cardList - rows * maxCols
|
|
|
|
if cardsForRow > maxCols then
|
|
|
|
cardsForRow = maxCols
|
|
|
|
end
|
|
|
|
position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)
|
|
|
|
position.x = position.x + SPREAD_X_SHIFT
|
|
|
|
cardsInRow = 0
|
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Spawn a specific list of cards. This method is for internal use and should not be called
|
|
|
|
-- directly, use spawnCards instead.
|
|
|
|
---@param cardList table A list of Player Card data structures (data/metadata)
|
|
|
|
---@param pos table Position where the cards should be spawned (global)
|
|
|
|
---@param rot table Rotation for the orientation of the spawned cards (global)
|
|
|
|
---@param callback? function callback to be called after the card/deck spawns.
|
|
|
|
Spawner.spawn = function(cardList, pos, rot, callback)
|
|
|
|
if #cardList == 0 then return end
|
|
|
|
|
|
|
|
-- Spawn a single card directly
|
|
|
|
if #cardList == 1 then
|
|
|
|
-- handle sideways card
|
|
|
|
if cardList[1].data.SidewaysCard then
|
|
|
|
rot = { rot.x, rot.y - 90, rot.z }
|
|
|
|
end
|
|
|
|
return spawnObjectData({
|
|
|
|
data = cardList[1].data,
|
|
|
|
position = pos,
|
|
|
|
rotation = rot,
|
|
|
|
callback_function = callback
|
2024-06-09 10:10:21 -04:00
|
|
|
})
|
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- For multiple cards, construct a deck and spawn that
|
|
|
|
local deckScaleX = cardList[1].data.Transform.scaleX
|
|
|
|
local deckScaleZ = cardList[1].data.Transform.scaleZ
|
|
|
|
local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ)
|
|
|
|
|
|
|
|
local sidewaysDeck = true
|
|
|
|
for _, spawnCard in ipairs(cardList) do
|
|
|
|
Spawner.addCardToDeck(deck, spawnCard.data)
|
|
|
|
-- set sidewaysDeck to false if any card is not a sideways card
|
|
|
|
sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
|
|
|
|
-- set the alt view angle for sideways decks
|
|
|
|
if sidewaysDeck then
|
|
|
|
deck.AltLookAngle = { x = 0, y = 180, z = 90 }
|
|
|
|
rot = { rot.x, rot.y - 90, rot.z }
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
|
|
|
|
return spawnObjectData({
|
|
|
|
data = deck,
|
|
|
|
position = pos,
|
|
|
|
rotation = rot,
|
|
|
|
callback_function = callback
|
2024-06-09 10:10:21 -04:00
|
|
|
})
|
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Inserts a card into the given deck. This does three things:
|
|
|
|
-- 1. Add the card's data to ContainedObjects
|
|
|
|
-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's
|
|
|
|
-- ID list. Note that the deck's ID list is "DeckIDs" even though it
|
|
|
|
-- contains a list of card Ids
|
|
|
|
-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's
|
|
|
|
-- "CustomDeck" field is a list of all CustomDecks used by cards within the
|
|
|
|
-- deck, keyed by the DeckID and referencing the custom deck table
|
|
|
|
---@param deck table TTS deck data structure to add to
|
|
|
|
---@param cardData table Data for the card to be inserted
|
|
|
|
Spawner.addCardToDeck = function(deck, cardData)
|
|
|
|
for customDeckId, customDeckData in pairs(cardData.CustomDeck) do
|
|
|
|
if (deck.CustomDeck[customDeckId] == nil) then
|
|
|
|
-- CustomDeck not added to deck yet, add it
|
|
|
|
deck.CustomDeck[customDeckId] = customDeckData
|
|
|
|
elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then
|
|
|
|
-- CustomDeck for this card matches the current one for the deck, do nothing
|
|
|
|
else
|
|
|
|
-- CustomDeck data conflict
|
|
|
|
local newDeckId = nil
|
|
|
|
for deckId, customDeck in pairs(deck.CustomDeck) do
|
|
|
|
if (customDeckData.FaceURL == customDeck.FaceURL) then
|
|
|
|
newDeckId = deckId
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if (newDeckId == nil) then
|
|
|
|
-- No non-conflicting custom deck for this card, add a new one
|
|
|
|
newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, "1000")
|
|
|
|
deck.CustomDeck[newDeckId] = customDeckData
|
|
|
|
end
|
|
|
|
-- Update the card with the new CustomDeck info
|
|
|
|
cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)
|
|
|
|
cardData.CustomDeck[customDeckId] = nil
|
|
|
|
cardData.CustomDeck[newDeckId] = customDeckData
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
table.insert(deck.ContainedObjects, cardData)
|
|
|
|
table.insert(deck.DeckIDs, cardData.CardID)
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2023-01-29 19:31:52 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Create an empty deck data table which can have cards added to it. This
|
|
|
|
-- creates a new table on each call without using metatables or previous
|
|
|
|
-- definitions because we can't be sure that TTS doesn't modify the structure
|
|
|
|
---@return table deck Table containing the minimal TTS deck data structure
|
|
|
|
Spawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ)
|
|
|
|
local deck = {}
|
|
|
|
deck.Name = "Deck"
|
|
|
|
|
|
|
|
-- Card data. DeckIDs and CustomDeck entries will be built from the cards
|
|
|
|
deck.ContainedObjects = {}
|
|
|
|
deck.DeckIDs = {}
|
|
|
|
deck.CustomDeck = {}
|
|
|
|
|
|
|
|
-- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here
|
|
|
|
-- Decks won't inherently scale to the cards in them. The card list being spawned should be all
|
|
|
|
-- the same type/size by this point, so use the first card to set the size
|
|
|
|
deck.Transform = {
|
|
|
|
scaleX = deckScaleX or 1,
|
|
|
|
scaleY = 1,
|
|
|
|
scaleZ = deckScaleZ or 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
return deck
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2023-08-27 21:09:46 -04:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Returns the first ID which does not exist in the given table, starting at startId and increasing
|
|
|
|
---@param objectTable table keyed by strings which are numbers
|
|
|
|
---@param startId string possible ID.
|
|
|
|
---@return string id >= startId
|
|
|
|
Spawner.findNextAvailableId = function(objectTable, startId)
|
|
|
|
local id = startId
|
|
|
|
while objectTable[id] ~= nil do
|
|
|
|
id = tostring(tonumber(id) + 1)
|
2023-08-27 21:09:46 -04:00
|
|
|
end
|
2024-07-27 21:47:52 -04:00
|
|
|
return id
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.
|
|
|
|
---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are
|
|
|
|
-- irrelevant as they provide only grouping and the order between them doesn't matter.
|
|
|
|
Spawner.getpbcn = function(metadata)
|
|
|
|
if metadata.permanent then
|
|
|
|
return 1
|
|
|
|
elseif metadata.bonded_to ~= nil then
|
|
|
|
return 2
|
|
|
|
else -- Normal card
|
|
|
|
return 3
|
2024-02-17 19:48:30 -05:00
|
|
|
end
|
2024-06-09 10:10:21 -04:00
|
|
|
end
|
2024-02-17 19:48:30 -05:00
|
|
|
|
2024-07-27 21:47:52 -04:00
|
|
|
-- Comparison function used to sort the cards in a deck. Groups bonded or
|
|
|
|
-- permanent cards first, then sorts within theose types by name/subname.
|
|
|
|
-- Normal cards will sort in standard alphabetical order, while
|
|
|
|
-- permanent/bonded/customizable will be in reverse alphabetical order.
|
|
|
|
--
|
|
|
|
-- Since cards spawn in the order provided by this comparator, with the first
|
|
|
|
-- cards ending up at the bottom of a pile, this ordering will spawn in reverse
|
|
|
|
-- alphabetical order. This presents the cards in order for non-face-down
|
|
|
|
-- areas, and presents them in order when Searching the face-down deck.
|
|
|
|
Spawner.cardComparator = function(card1, card2)
|
|
|
|
local pbcn1 = Spawner.getpbcn(card1.metadata)
|
|
|
|
local pbcn2 = Spawner.getpbcn(card2.metadata)
|
|
|
|
if pbcn1 ~= pbcn2 then
|
|
|
|
return pbcn1 > pbcn2
|
|
|
|
end
|
|
|
|
if pbcn1 == 3 then
|
|
|
|
if card1.data.Nickname ~= card2.data.Nickname then
|
|
|
|
return card1.data.Nickname < card2.data.Nickname
|
|
|
|
end
|
|
|
|
return card1.data.Description < card2.data.Description
|
|
|
|
else
|
|
|
|
if card1.data.Nickname ~= card2.data.Nickname then
|
|
|
|
return card1.data.Nickname > card2.data.Nickname
|
|
|
|
end
|
|
|
|
return card1.data.Description > card2.data.Description
|
2023-08-27 21:09:46 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end)
|
2024-07-27 21:47:52 -04:00
|
|
|
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
|
|
require("playercards/PlayerCardPanel")
|
|
|
|
end)
|
2023-01-29 19:31:52 -05:00
|
|
|
return __bundle_require("__root")
|