2442 lines
97 KiB
Plaintext
2442 lines
97 KiB
Plaintext
-- Bundled by luabundle {"version":"1.6.0"}
|
|
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
|
|
local loadingPlaceholder = {[{}] = true}
|
|
|
|
local register
|
|
local modules = {}
|
|
|
|
local require
|
|
local loaded = {}
|
|
|
|
register = function(name, body)
|
|
if not modules[name] then
|
|
modules[name] = body
|
|
end
|
|
end
|
|
|
|
require = function(name)
|
|
local loadedModule = loaded[name]
|
|
|
|
if loadedModule then
|
|
if loadedModule == loadingPlaceholder then
|
|
return nil
|
|
end
|
|
else
|
|
if not modules[name] then
|
|
if not superRequire then
|
|
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
|
|
error('Tried to require ' .. identifier .. ', but no such module has been registered')
|
|
else
|
|
return superRequire(name)
|
|
end
|
|
end
|
|
|
|
loaded[name] = loadingPlaceholder
|
|
loadedModule = modules[name](require, loaded, register, modules)
|
|
loaded[name] = loadedModule
|
|
end
|
|
|
|
return loadedModule
|
|
end
|
|
|
|
return require, loaded, register, modules
|
|
end)(nil)
|
|
__bundle_register("arkhamdb/DeckImporter", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
require("playercards/PlayerCardSpawner")
|
|
|
|
local allCardsBagApi = require("playercards/AllCardsBagApi")
|
|
local arkhamDb = require("arkhamdb/ArkhamDb")
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
|
local playermatApi = require("playermat/PlayermatApi")
|
|
local zones = require("playermat/Zones")
|
|
|
|
local matsWithInvestigator = {}
|
|
local startsInPlayCount = 0
|
|
|
|
local INPUT_FIELD_HEIGHT = 340
|
|
local INPUT_FIELD_WIDTH = 1500
|
|
local FIELD_COLOR = { 0.9, 0.7, 0.5 }
|
|
|
|
local PRIVATE_TOGGLE_LABELS = {}
|
|
PRIVATE_TOGGLE_LABELS[true] = "Private"
|
|
PRIVATE_TOGGLE_LABELS[false] = "Published"
|
|
|
|
local UPGRADED_TOGGLE_LABELS = {}
|
|
UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
|
|
UPGRADED_TOGGLE_LABELS[false] = "Specific"
|
|
|
|
local LOAD_INVESTIGATOR_TOGGLE_LABELS = {}
|
|
LOAD_INVESTIGATOR_TOGGLE_LABELS[true] = "Yes"
|
|
LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No"
|
|
|
|
local redDeckId = ""
|
|
local orangeDeckId = ""
|
|
local whiteDeckId = ""
|
|
local greenDeckId = ""
|
|
|
|
local privateDeck = true
|
|
local loadNewestDeck = true
|
|
local loadInvestigators = false
|
|
|
|
function onLoad(script_state)
|
|
initializeUi(JSON.decode(script_state))
|
|
math.randomseed(os.time())
|
|
arkhamDb.initialize()
|
|
end
|
|
|
|
function arkhamdb_reinit()
|
|
arkhamDb.initialize()
|
|
end
|
|
|
|
function onSave() return JSON.encode(getUiState()) end
|
|
|
|
-- Returns a table with the full state of the UI, including options and deck IDs.
|
|
-- This can be used to persist via onSave(), or provide values for a load operation
|
|
---@return uiStateTable uiStateTable Contains data about the current UI state
|
|
function getUiState()
|
|
return {
|
|
redDeck = redDeckId,
|
|
orangeDeck = orangeDeckId,
|
|
whiteDeck = whiteDeckId,
|
|
greenDeck = greenDeckId,
|
|
privateDeck = privateDeck,
|
|
loadNewest = loadNewestDeck,
|
|
investigators = loadInvestigators
|
|
}
|
|
end
|
|
|
|
-- Updates the state of the UI based on the provided table. Any values not provided will be left the same.
|
|
---@param uiStateTable table Table of values to update on importer
|
|
function setUiState(uiStateTable)
|
|
self.clearButtons()
|
|
self.clearInputs()
|
|
initializeUi(uiStateTable)
|
|
end
|
|
|
|
-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()
|
|
function initializeUi(savedUiState)
|
|
if savedUiState ~= nil then
|
|
redDeckId = savedUiState.redDeck
|
|
orangeDeckId = savedUiState.orangeDeck
|
|
whiteDeckId = savedUiState.whiteDeck
|
|
greenDeckId = savedUiState.greenDeck
|
|
privateDeck = savedUiState.privateDeck
|
|
loadNewestDeck = savedUiState.loadNewest
|
|
loadInvestigators = savedUiState.investigators
|
|
end
|
|
|
|
makeOptionToggles()
|
|
makeDeckIdFields()
|
|
makeBuildButton()
|
|
end
|
|
|
|
function makeOptionToggles()
|
|
-- common parameters
|
|
local checkbox_parameters = {}
|
|
checkbox_parameters.function_owner = self
|
|
checkbox_parameters.width = INPUT_FIELD_WIDTH
|
|
checkbox_parameters.height = INPUT_FIELD_HEIGHT
|
|
checkbox_parameters.scale = { 0.1, 0.1, 0.1 }
|
|
checkbox_parameters.font_size = 240
|
|
checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 }
|
|
checkbox_parameters.color = FIELD_COLOR
|
|
|
|
-- public / private deck
|
|
checkbox_parameters.click_function = "publicPrivateChanged"
|
|
checkbox_parameters.position = { 0.25, 0.1, -0.102 }
|
|
checkbox_parameters.tooltip = "Published or private deck?\n\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!"
|
|
checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]
|
|
self.createButton(checkbox_parameters)
|
|
|
|
-- load upgraded?
|
|
checkbox_parameters.click_function = "loadUpgradedChanged"
|
|
checkbox_parameters.position = { 0.25, 0.1, -0.01 }
|
|
checkbox_parameters.tooltip = "Load newest upgrade or exact deck?"
|
|
checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]
|
|
self.createButton(checkbox_parameters)
|
|
|
|
-- load investigators?
|
|
checkbox_parameters.click_function = "loadInvestigatorsChanged"
|
|
checkbox_parameters.position = { 0.25, 0.1, 0.081 }
|
|
checkbox_parameters.tooltip = "Spawn investigator cards?"
|
|
checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]
|
|
self.createButton(checkbox_parameters)
|
|
end
|
|
|
|
-- Create the four deck ID entry fields
|
|
function makeDeckIdFields()
|
|
local input_parameters = {}
|
|
-- Parameters common to all entry fields
|
|
input_parameters.function_owner = self
|
|
input_parameters.scale = { 0.1, 0.1, 0.1 }
|
|
input_parameters.width = INPUT_FIELD_WIDTH
|
|
input_parameters.height = INPUT_FIELD_HEIGHT
|
|
input_parameters.font_size = 320
|
|
input_parameters.tooltip = "Deck ID from ArkhamDB URL of the deck\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'"
|
|
input_parameters.alignment = 3 -- Center
|
|
input_parameters.color = FIELD_COLOR
|
|
input_parameters.font_color = { 0, 0, 0 }
|
|
input_parameters.validation = 2 -- Integer
|
|
|
|
-- Green
|
|
input_parameters.input_function = "greenDeckChanged"
|
|
input_parameters.position = { -0.166, 0.1, 0.385 }
|
|
input_parameters.value = greenDeckId
|
|
self.createInput(input_parameters)
|
|
-- Red
|
|
input_parameters.input_function = "redDeckChanged"
|
|
input_parameters.position = { 0.171, 0.1, 0.385 }
|
|
input_parameters.value = redDeckId
|
|
self.createInput(input_parameters)
|
|
-- White
|
|
input_parameters.input_function = "whiteDeckChanged"
|
|
input_parameters.position = { -0.166, 0.1, 0.474 }
|
|
input_parameters.value = whiteDeckId
|
|
self.createInput(input_parameters)
|
|
-- Orange
|
|
input_parameters.input_function = "orangeDeckChanged"
|
|
input_parameters.position = { 0.171, 0.1, 0.474 }
|
|
input_parameters.value = orangeDeckId
|
|
self.createInput(input_parameters)
|
|
end
|
|
|
|
-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic
|
|
function makeBuildButton()
|
|
local button_parameters = {}
|
|
button_parameters.click_function = "loadDecks"
|
|
button_parameters.function_owner = self
|
|
button_parameters.position = { 0, 0.1, 0.71 }
|
|
button_parameters.width = 320
|
|
button_parameters.height = 30
|
|
button_parameters.color = { 0, 0, 0, 0 }
|
|
button_parameters.tooltip = "Click to build all four decks!"
|
|
self.createButton(button_parameters)
|
|
end
|
|
|
|
-- Event handlers for deck ID change
|
|
function redDeckChanged(_, _, inputValue) redDeckId = inputValue end
|
|
function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end
|
|
function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end
|
|
function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end
|
|
|
|
-- Event handlers for toggle buttons
|
|
function publicPrivateChanged()
|
|
privateDeck = not privateDeck
|
|
self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }
|
|
end
|
|
|
|
function loadUpgradedChanged()
|
|
loadNewestDeck = not loadNewestDeck
|
|
self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }
|
|
end
|
|
|
|
function loadInvestigatorsChanged()
|
|
loadInvestigators = not loadInvestigators
|
|
self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }
|
|
end
|
|
|
|
function loadDecks()
|
|
if not allCardsBagApi.isIndexReady() then return end
|
|
matsWithInvestigator = playermatApi.getUsedMatColors()
|
|
if redDeckId ~= nil and redDeckId ~= "" then
|
|
buildDeck("Red", redDeckId)
|
|
end
|
|
if orangeDeckId ~= nil and orangeDeckId ~= "" then
|
|
buildDeck("Orange", orangeDeckId)
|
|
end
|
|
if whiteDeckId ~= nil and whiteDeckId ~= "" then
|
|
buildDeck("White", whiteDeckId)
|
|
end
|
|
if greenDeckId ~= nil and greenDeckId ~= "" then
|
|
buildDeck("Green", greenDeckId)
|
|
end
|
|
end
|
|
|
|
-- Returns the zone name where the specified card should be placed, based on its metadata.
|
|
---@param cardMetadata table Contains card metadata
|
|
---@return string Zone Name of the zone such as "Deck", "SetAside1", etc. (See zones file for a list of valid zones)
|
|
function getDefaultCardZone(cardMetadata, bondedList)
|
|
if cardMetadata.id == "09080-m" then -- Have to check the Servitor before other minicards
|
|
return "SetAside6"
|
|
elseif cardMetadata.id == "09006" then -- On The Mend is set aside
|
|
return "SetAside2"
|
|
elseif bondedList[cardMetadata.id] then
|
|
return "SetAside2"
|
|
elseif cardMetadata.type == "Investigator" then
|
|
return "Investigator"
|
|
elseif cardMetadata.type == "Minicard" then
|
|
return "Minicard"
|
|
elseif cardMetadata.type == "UpgradeSheet" then
|
|
return "SetAside4"
|
|
elseif cardMetadata.startsInPlay then
|
|
return startsInPlayTracker()
|
|
elseif cardMetadata.permanent then
|
|
return "SetAside1"
|
|
-- SetAside3 is used for Ancestral Knowledge / Underworld Market
|
|
else
|
|
return "Deck"
|
|
end
|
|
end
|
|
|
|
function startsInPlayTracker()
|
|
startsInPlayCount = startsInPlayCount + 1
|
|
if startsInPlayCount > 6 then
|
|
broadcastToAll("Card that should start in play was placed with permanents because no blank slots remained")
|
|
return "SetAside1"
|
|
else
|
|
return "Blank" .. startsInPlayCount
|
|
end
|
|
end
|
|
|
|
function buildDeck(playerColor, deckId)
|
|
local uiState = getUiState()
|
|
arkhamDb.getDecklist(
|
|
playerColor,
|
|
deckId,
|
|
uiState.privateDeck,
|
|
uiState.loadNewest,
|
|
uiState.investigators,
|
|
loadCards)
|
|
end
|
|
|
|
-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards
|
|
-- at the appropriate zones and report an error to the user if any could not be loaded.
|
|
-- This is a callback function which handles the results of ArkhamDb.getDecklist()
|
|
-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.
|
|
---@param slots table Key-Value table of cardId:count
|
|
---@param investigatorId string ArkhamDB ID (code) for this deck's investigator.
|
|
-- Investigator cards should already be added to the slots list if they should be spawned,
|
|
-- but this value is separate to check for special handling for certain investigators
|
|
---@param bondedList table A table of cardID keys to meaningless values. Card IDs in this list were added
|
|
-- from a parent bonded card.
|
|
---@param customizations table ArkhamDB data for customizations on customizable cards
|
|
---@param playerColor string Color name of the player mat to place this deck on (e.g. "Red")
|
|
---@param loadAltInvestigator string Contains the name of alternative art for the investigator ("normal", "revised" or "promo")
|
|
function loadCards(slots, investigatorId, bondedList, customizations, playerColor, loadAltInvestigator)
|
|
function coinside()
|
|
local cardsToSpawn = {}
|
|
local resourceModifier = 0
|
|
|
|
-- reset the startsInPlayCount
|
|
startsInPlayCount = 0
|
|
for cardId, cardCount in pairs(slots) do
|
|
local card = allCardsBagApi.getCardById(cardId)
|
|
if card ~= nil then
|
|
local cardZone = getDefaultCardZone(card.metadata, bondedList)
|
|
for i = 1, cardCount do
|
|
table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
|
|
end
|
|
slots[cardId] = 0
|
|
end
|
|
|
|
-- check for resource modifiers
|
|
if cardId == "02037" then -- Indebted
|
|
resourceModifier = resourceModifier - 2 * cardCount
|
|
elseif cardId == "05278" then -- Another Day, Another Dollar
|
|
resourceModifier = resourceModifier + 2 * cardCount
|
|
end
|
|
end
|
|
|
|
updateStartingResources(playerColor, resourceModifier)
|
|
handleAncestralKnowledge(cardsToSpawn)
|
|
handleUnderworldMarket(cardsToSpawn, playerColor)
|
|
handleHunchDeck(investigatorId, cardsToSpawn, bondedList, playerColor)
|
|
handleSpiritDeck(investigatorId, cardsToSpawn, playerColor, customizations)
|
|
handleCustomizableUpgrades(cardsToSpawn, customizations)
|
|
handlePeteSignatureAssets(investigatorId, cardsToSpawn)
|
|
|
|
-- Split the card list into separate lists for each zone
|
|
local zoneDecks = buildZoneLists(cardsToSpawn)
|
|
|
|
-- Check for existing cards in zones and maybe skip them
|
|
removeBusyZones(playerColor, zoneDecks)
|
|
|
|
-- Spawn the list for each zone
|
|
for zone, zoneCards in pairs(zoneDecks) do
|
|
local deckPos = zones.getZonePosition(playerColor, zone):setAt("y", 3)
|
|
local deckRot = zones.getDefaultCardRotation(playerColor, zone)
|
|
local callback = nil
|
|
|
|
-- If cards are spread too close together TTS groups them weirdly, selecting multiples
|
|
-- when hovering over a single card. This distance is the minimum to avoid that.
|
|
local spreadDistance = 1.15
|
|
if (zone == "SetAside4") then
|
|
-- SetAside4 is reserved for customization cards, and we want them spread on the table
|
|
-- so their checkboxes are visible
|
|
-- TO-DO: take into account that spreading will make multiple rows
|
|
-- (this is affected by the user's local settings!)
|
|
if (playerColor == "White") then
|
|
deckPos.z = deckPos.z + (#zoneCards - 1) * spreadDistance
|
|
elseif (playerColor == "Green") then
|
|
deckPos.x = deckPos.x + (#zoneCards - 1) * spreadDistance
|
|
end
|
|
callback = function(deck) deck.spread(spreadDistance) end
|
|
elseif zone == "Deck" then
|
|
callback = function(deck) deckSpawned(deck, playerColor) end
|
|
elseif zone == "Investigator" or zone == "Minicard" then
|
|
callback = function(card) loadAltArt(card, loadAltInvestigator) end
|
|
end
|
|
Spawner.spawnCards(zoneCards, deckPos, deckRot, true, callback)
|
|
coroutine.yield(0)
|
|
end
|
|
|
|
-- Look for any cards which haven't been loaded
|
|
local hadError = false
|
|
for cardId, remainingCount in pairs(slots) do
|
|
if remainingCount > 0 then
|
|
hadError = true
|
|
arkhamDb.logCardNotFound(cardId, playerColor)
|
|
end
|
|
end
|
|
if (not hadError) then
|
|
printToAll("Deck loaded successfully!", playerColor)
|
|
end
|
|
return 1
|
|
end
|
|
|
|
startLuaCoroutine(self, "coinside")
|
|
end
|
|
|
|
-- Callback handler for the main deck spawning. Looks for cards which should start in hand, and
|
|
-- draws them for the appropriate player.
|
|
---@param deck tts__Object Callback-provided spawned deck object
|
|
---@param playerColor string Color of the player to draw the cards to
|
|
function deckSpawned(deck, playerColor)
|
|
local player = Player[playermatApi.getPlayerColor(playerColor)]
|
|
local handPos = player.getHandTransform(1).position -- Only one hand zone per player
|
|
local deckCards = deck.getData().ContainedObjects
|
|
|
|
-- Process in reverse order so taking cards out doesn't upset the indexing
|
|
for i = #deckCards, 1, -1 do
|
|
local cardMetadata = JSON.decode(deckCards[i].GMNotes) or { }
|
|
if cardMetadata.startsInHand then
|
|
deck.takeObject({ index = i - 1, position = handPos, flip = true, smooth = true})
|
|
end
|
|
end
|
|
|
|
-- add the "PlayerCard" tag to the deck
|
|
if deck and deck.type == "Deck" and deck.getQuantity() > 1 then
|
|
deck.addTag("PlayerCard")
|
|
end
|
|
end
|
|
|
|
-- Converts the Raven Quill's selections from card IDs to card names. This could be more elegant
|
|
-- but the inputs are very static so we're using some brute force.
|
|
---@param selectionString string provided by ArkhamDB, indicates the customization selections
|
|
-- Should be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY)
|
|
function convertRavenQuillSelections(selectionString)
|
|
if string.len(selectionString) == 5 then
|
|
return getCardName(selectionString)
|
|
elseif string.len(selectionString) == 11 then
|
|
return getCardName(string.sub(selectionString, 1, 5)) .. ", " .. getCardName(string.sub(selectionString, 7))
|
|
end
|
|
end
|
|
|
|
-- Converts Grizzled's selections from a single string with "^".
|
|
---@param selectionString string provided by ArkhamDB, indicates the customization selections
|
|
-- Should be two traits separated by a ^ (e.g. XXXXX^YYYYY)
|
|
function convertGrizzledSelections(selectionString)
|
|
return selectionString:gsub("%^", ", ")
|
|
end
|
|
|
|
-- Returns the simple name of a card given its ID. This will find the card and strip any trailing
|
|
-- SCED-specific suffixes such as (Taboo) or (Level)
|
|
function getCardName(cardId)
|
|
local card = allCardsBagApi.getCardById(cardId)
|
|
if card then
|
|
local name = card.data.Nickname
|
|
if string.find(name, " %(") then
|
|
return string.sub(name, 1, string.find(name, " %(") - 1)
|
|
else
|
|
return name
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Split a single list of cards into a separate table of lists, keyed by the zone
|
|
---@param cards table Table of {cardData, cardMetadata, zone}
|
|
---@return table zoneDecks Table with zoneName as index: {zoneName=card list}
|
|
function buildZoneLists(cards)
|
|
local zoneList = {}
|
|
for _, card in ipairs(cards) do
|
|
if zoneList[card.zone] == nil then
|
|
zoneList[card.zone] = {}
|
|
end
|
|
table.insert(zoneList[card.zone], card)
|
|
end
|
|
|
|
return zoneList
|
|
end
|
|
|
|
-- removes zones from list if they are occupied
|
|
---@param playerColor string Color this deck is being loaded for
|
|
---@param zoneDecks table Table with zoneName as index: {zoneName=card list}
|
|
function removeBusyZones(playerColor, zoneDecks)
|
|
-- check for existing investigator
|
|
for _, matColor in ipairs(matsWithInvestigator) do
|
|
if matColor == playerColor then
|
|
zoneDecks["Investigator"] = nil
|
|
printToAll("Skipped investigator import", playerColor)
|
|
break
|
|
end
|
|
end
|
|
|
|
-- check for existing minicard
|
|
local mat = guidReferenceApi.getObjectByOwnerAndType(playerColor, "Playermat")
|
|
local miniId = mat.getVar("activeInvestigatorId") .. "-m"
|
|
|
|
-- remove taboo suffix since we don't have this for minicards
|
|
miniId = miniId:gsub("-t", "")
|
|
|
|
for _, obj in ipairs(getObjectsWithTag("Minicard")) do
|
|
local notes = JSON.decode(obj.getGMNotes())
|
|
if notes ~= nil and notes.id == miniId then
|
|
local pos = mat.positionToWorld(Vector(-1.36, 0, -0.625)):setAt("y", 1.67)
|
|
obj.setPosition(pos)
|
|
zoneDecks["Minicard"] = nil
|
|
printToAll("Skipped minicard import", playerColor)
|
|
break
|
|
end
|
|
end
|
|
|
|
-- check for existing deck
|
|
local cardsInDeckArea = 0
|
|
for _, obj in pairs(playermatApi.getDeckAreaObjects(playerColor)) do
|
|
cardsInDeckArea = cardsInDeckArea + #obj.getObjects()
|
|
end
|
|
|
|
-- threshhold of 16 cards for skipping deck import to cover cases like Tekeli-li cards
|
|
if cardsInDeckArea > 16 then
|
|
for i = 1, 6 do
|
|
zoneDecks["SetAside" .. i] = nil
|
|
zoneDecks["Blank" .. i] = nil
|
|
end
|
|
zoneDecks["Deck"] = nil
|
|
printToAll("Skipped deck import", playerColor)
|
|
end
|
|
end
|
|
|
|
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3
|
|
---@param cardList table Deck list being created
|
|
function handleAncestralKnowledge(cardList)
|
|
local hasAncestralKnowledge = false
|
|
local skillList = {}
|
|
|
|
-- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass
|
|
for i, card in ipairs(cardList) do
|
|
if card.metadata.id == "07303" then
|
|
hasAncestralKnowledge = true
|
|
card.zone = "SetAside3"
|
|
elseif (card.metadata.type == "Skill"
|
|
and card.zone == "Deck"
|
|
and not card.metadata.weakness) then
|
|
table.insert(skillList, i)
|
|
end
|
|
end
|
|
|
|
if not hasAncestralKnowledge then return end
|
|
|
|
-- Move 5 random skills to SetAside3
|
|
for i = 1, 5 do
|
|
local skillListIndex = math.random(#skillList)
|
|
cardList[skillList[skillListIndex]].zone = "UnderSetAside3"
|
|
table.remove(skillList, skillListIndex)
|
|
end
|
|
end
|
|
|
|
-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3
|
|
---@param cardList table Deck list being created
|
|
---@param playerColor string Color this deck is being loaded for
|
|
function handleUnderworldMarket(cardList, playerColor)
|
|
local hasMarket = false
|
|
local illicitList = {}
|
|
|
|
-- Process the entire list to check for Underworld Market and get all possible Illicit cards, doing both in one pass
|
|
for i, card in ipairs(cardList) do
|
|
if card.metadata.id == "09077" then
|
|
hasMarket = true
|
|
card.zone = "SetAside3"
|
|
elseif card.metadata.traits ~= nil and string.find(card.metadata.traits, "Illicit", 1, true) and card.zone == "Deck" then
|
|
table.insert(illicitList, i)
|
|
end
|
|
end
|
|
|
|
if not hasMarket then return end
|
|
|
|
if #illicitList < 10 then
|
|
printToAll("Only " .. #illicitList .. " Illicit cards in your deck, you can't trigger Underworld Market's ability.", playerColor)
|
|
else
|
|
-- Process cards to move them to the market deck. This is done in reverse order because the sorting needs
|
|
-- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)
|
|
-- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem
|
|
for i = #illicitList, 1, -1 do
|
|
local moving = cardList[illicitList[i]]
|
|
moving.zone = "UnderSetAside3"
|
|
table.remove(cardList, illicitList[i])
|
|
table.insert(cardList, moving)
|
|
end
|
|
|
|
if #illicitList > 10 then
|
|
printToAll("Moved all " .. #illicitList .. " Illicit cards to the Market deck, reduce it to 10", playerColor)
|
|
else
|
|
printToAll("Built the Market deck", playerColor)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck
|
|
---@param investigatorId string ID for the deck's investigator card
|
|
---@param cardList table Deck list being created
|
|
---@param playerColor string Color this deck is being loaded for
|
|
function handleHunchDeck(investigatorId, cardList, bondedList, playerColor)
|
|
if investigatorId ~= "05002" then return end
|
|
|
|
local insightList = {}
|
|
for i, card in ipairs(cardList) do
|
|
if (card.metadata.type == "Event"
|
|
and card.metadata.traits ~= nil
|
|
and string.match(card.metadata.traits, "Insight")
|
|
and bondedList[card.metadata.id] == nil) then
|
|
table.insert(insightList, i)
|
|
end
|
|
end
|
|
-- Process cards to move them to the hunch deck. This is done in reverse order because the sorting needs
|
|
-- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)
|
|
-- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem
|
|
for i = #insightList, 1, -1 do
|
|
local moving = cardList[insightList[i]]
|
|
moving.zone = "SetAside5"
|
|
table.remove(cardList, insightList[i])
|
|
table.insert(cardList, moving)
|
|
end
|
|
|
|
if #insightList < 11 then
|
|
printToAll("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList .. " Insight events.", playerColor)
|
|
elseif #insightList > 11 then
|
|
printToAll("Moved all " .. #insightList .. " Insight events to the hunch deck, reduce it to 11.", playerColor)
|
|
else
|
|
printToAll("Built Joe's hunch deck", playerColor)
|
|
end
|
|
end
|
|
|
|
-- If the investigator is Parallel Jim Culver, extract all Ally assets to SetAside5 to build the Spirit Deck
|
|
---@param investigatorId string ID for the deck's investigator card
|
|
---@param cardList table Deck list being created
|
|
---@param playerColor string Color this deck is being loaded for
|
|
---@param customizations table Additional deck information
|
|
function handleSpiritDeck(investigatorId, cardList, playerColor, customizations)
|
|
if investigatorId ~= "02004-p" and investigatorId ~= "02004-pb" then return end
|
|
|
|
local spiritList = {}
|
|
if customizations["extra_deck"] then
|
|
-- split by ","
|
|
for str in string.gmatch(customizations["extra_deck"], "([^,]+)") do
|
|
local card = allCardsBagApi.getCardById(str)
|
|
if card ~= nil then
|
|
table.insert(cardList, { data = card.data, metadata = card.metadata, zone = "SetAside5" })
|
|
table.insert(spiritList, str)
|
|
end
|
|
end
|
|
else
|
|
for i, card in ipairs(cardList) do
|
|
if card.metadata.id == "90053" or (
|
|
card.metadata.type == "Asset"
|
|
and card.metadata.traits ~= nil
|
|
and string.match(card.metadata.traits, "Ally")
|
|
and card.metadata.level ~= nil
|
|
and card.metadata.level < 3) then
|
|
table.insert(spiritList, i)
|
|
end
|
|
end
|
|
|
|
-- Process cards to move them to the spirit deck. This is done in reverse order because the sorting needs
|
|
-- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)
|
|
-- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem
|
|
for i = #spiritList, 1, -1 do
|
|
local moving = cardList[spiritList[i]]
|
|
moving.zone = "SetAside5"
|
|
table.remove(cardList, spiritList[i])
|
|
table.insert(cardList, moving)
|
|
end
|
|
end
|
|
|
|
if #spiritList < 10 then
|
|
printToAll("Jim's spirit deck must have 9 Ally assets but the deck only has " .. (#spiritList - 1) .. " Ally assets.", playerColor)
|
|
elseif #spiritList > 11 then
|
|
printToAll("Moved all " .. (#spiritList - 1) .. " Ally assets to the spirit deck, reduce it to 10 (including Vengeful Shade).", playerColor)
|
|
else
|
|
printToAll("Built Jim's spirit deck", playerColor)
|
|
end
|
|
end
|
|
|
|
-- For any customization upgrade cards in the card list, process the metadata from the deck to
|
|
-- set the save state to show the correct checkboxes/text field values
|
|
---@param cardList table Deck list being created
|
|
---@param customizations table ArkhamDB data for customizations on customizable cards
|
|
function handleCustomizableUpgrades(cardList, customizations)
|
|
for _, card in ipairs(cardList) do
|
|
if card.metadata.type == "UpgradeSheet" then
|
|
local baseId = string.sub(card.metadata.id, 1, 5)
|
|
local upgrades = customizations["cus_" .. baseId]
|
|
|
|
if upgrades ~= nil then
|
|
-- contains the amount of markedBoxes (left to right) per row (starting at row 1)
|
|
local selectedUpgrades = {}
|
|
|
|
-- contains the amount of inputValues per row (starting at row 0)
|
|
local index_xp = {}
|
|
|
|
-- get the index and xp values (looks like this: X|X,X|X, ..)
|
|
-- input string from ArkhamDB is split by ","
|
|
for str in string.gmatch(customizations["cus_" .. baseId], "([^,]+)") do
|
|
table.insert(index_xp, str)
|
|
end
|
|
|
|
-- split each pair and assign it to the proper position in markedBoxes
|
|
for _, entry in ipairs(index_xp) do
|
|
-- counter increments from 1 to 3 and indicates the part of the string we are on
|
|
-- usually: 1 = row, 2 = amount of check boxes, 3 = entry in inputfield
|
|
local counter = 0
|
|
local row = 0
|
|
|
|
-- parsing the string for each row
|
|
for str in entry:gmatch("([^|]+)") do
|
|
counter = counter + 1
|
|
|
|
if counter == 1 then
|
|
row = tonumber(str) + 1
|
|
elseif counter == 2 then
|
|
if selectedUpgrades[row] == nil then
|
|
selectedUpgrades[row] = { }
|
|
end
|
|
selectedUpgrades[row].xp = tonumber(str)
|
|
elseif counter == 3 and str ~= "" then
|
|
if baseId == "09042" then
|
|
selectedUpgrades[row].text = convertRavenQuillSelections(str)
|
|
elseif baseId == "09101" then
|
|
selectedUpgrades[row].text = convertGrizzledSelections(str)
|
|
elseif baseId == "09079" then -- Living Ink skill selection
|
|
-- All skills, regardless of row, are placed in upgrade slot 1 as a comma-delimited list
|
|
if selectedUpgrades[1] == nil then
|
|
selectedUpgrades[1] = { }
|
|
end
|
|
if selectedUpgrades[1].text == nil then
|
|
selectedUpgrades[1].text = str
|
|
else
|
|
selectedUpgrades[1].text = selectedUpgrades[1].text .. "," .. str
|
|
end
|
|
else
|
|
selectedUpgrades[row].text = str
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- write the loaded values to the save_data of the sheets
|
|
card.data["LuaScriptState"] = JSON.encode({ selections = selectedUpgrades })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handles cards that start in play under specific conditions for Ashcan Pete (Regular Pete - Duke, Parallel Pete - Guitar)
|
|
---@param investigatorId string ID for the deck's investigator card
|
|
---@param cardList table Deck list being created
|
|
function handlePeteSignatureAssets(investigatorId, cardList)
|
|
if investigatorId == "02005" or investigatorId == "02005-pb" then -- regular Pete's front
|
|
for _, card in ipairs(cardList) do
|
|
if card.metadata.id == "02014" then -- Duke
|
|
card.zone = startsInPlayTracker()
|
|
end
|
|
end
|
|
elseif investigatorId == "02005-p" or investigatorId == "02005-pf" then -- parallel Pete's front
|
|
for _, card in ipairs(cardList) do
|
|
if card.metadata.id == "90047" then -- Pete's Guitar
|
|
card.zone = startsInPlayTracker()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Callback function for investigator cards and minicards to set the correct state for alt art
|
|
---@param card tts__Object Card which needs to be set the state for
|
|
---@param loadAltInvestigator string Contains the name of alternative art for the investigator ("normal", "revised" or "promo")
|
|
function loadAltArt(card, loadAltInvestigator)
|
|
-- states are set up this way:
|
|
-- 1 - normal, 2 - revised/promo, 3 - promo (if 2 is revised)
|
|
-- This means we can always load the 2nd state for revised and just get the last state for promo
|
|
if loadAltInvestigator == "normal" then
|
|
return
|
|
elseif loadAltInvestigator == "revised" then
|
|
card.setState(2)
|
|
elseif loadAltInvestigator == "promo" then
|
|
local states = card.getStates()
|
|
card.setState(#states)
|
|
end
|
|
end
|
|
|
|
-- updates the starting resources
|
|
---@param playerColor string Color this deck is being loaded for
|
|
---@param resourceModifier number Modifier for the starting resources
|
|
function updateStartingResources(playerColor, resourceModifier)
|
|
if resourceModifier ~= 0 then
|
|
playermatApi.updateCounter(playerColor, "ResourceCounter", _, resourceModifier)
|
|
printToAll("Modified starting resources", playerColor)
|
|
end
|
|
end
|
|
end)
|
|
__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local GUIDReferenceApi = {}
|
|
|
|
local function getGuidHandler()
|
|
return getObjectFromGUID("123456")
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- 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
|
|
|
|
-- 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)
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- 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)
|
|
end
|
|
|
|
return GUIDReferenceApi
|
|
end
|
|
end)
|
|
__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local AllCardsBagApi = {}
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
|
|
|
local function getAllCardsBag()
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag")
|
|
end
|
|
|
|
-- 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
|
|
end
|
|
|
|
-- 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 })
|
|
end
|
|
|
|
-- 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")
|
|
end
|
|
|
|
AllCardsBagApi.isIndexReady = function()
|
|
return getAllCardsBag().call("isIndexReady")
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- 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 }))
|
|
end
|
|
|
|
AllCardsBagApi.isBagPresent = function()
|
|
return getAllCardsBag() and true
|
|
end
|
|
|
|
-- 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 }))
|
|
end
|
|
|
|
-- 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 }))
|
|
end
|
|
|
|
-- 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))
|
|
end
|
|
|
|
AllCardsBagApi.getUniqueWeaknesses = function()
|
|
return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses"))
|
|
end
|
|
|
|
return AllCardsBagApi
|
|
end
|
|
end)
|
|
__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local PlayermatApi = {}
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
|
local searchLib = require("util/SearchLib")
|
|
local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }
|
|
|
|
-- Convenience function to look up a mat's object by color, or get all mats.
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@return table: Single-element if only single playermat is requested
|
|
local function getMatForColor(matColor)
|
|
if matColor == "All" then
|
|
return guidReferenceApi.getObjectsByType("Playermat")
|
|
else
|
|
return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") }
|
|
end
|
|
end
|
|
|
|
-- Returns the color of the closest playermat
|
|
---@param startPos table Starting position to get the closest mat from
|
|
PlayermatApi.getMatColorByPosition = function(startPos)
|
|
local result, smallestDistance
|
|
for matColor, mat in pairs(getMatForColor("All")) do
|
|
local distance = Vector.between(startPos, mat.getPosition()):magnitude()
|
|
if smallestDistance == nil or distance < smallestDistance then
|
|
smallestDistance = distance
|
|
result = matColor
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Returns the color of the player's hand that is seated next to the playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getPlayerColor = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getVar("playerColor")
|
|
end
|
|
end
|
|
|
|
-- Returns the color of the playermat that owns the playercolor's hand
|
|
---@param handColor string Color of the playermat
|
|
PlayermatApi.getMatColor = function(handColor)
|
|
for matColor, mat in pairs(getMatForColor("All")) do
|
|
local playerColor = mat.getVar("playerColor")
|
|
if playerColor == handColor then
|
|
return matColor
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Instructs a playermat to check for DES
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.checkForDES = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("checkForDES")
|
|
end
|
|
end
|
|
|
|
-- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
---@return boolean: whether DES is present on the playermat
|
|
PlayermatApi.hasDES = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getVar("hasDES")
|
|
end
|
|
end
|
|
|
|
-- gets the slot data for the playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getSlotData = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getTable("slotData")
|
|
end
|
|
end
|
|
|
|
-- sets the slot data for the playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
---@param newSlotData table New slot data for the playermat
|
|
PlayermatApi.loadSlotData = function(matColor, newSlotData)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.setTable("slotData", newSlotData)
|
|
mat.call("redrawSlotSymbols")
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Performs a search of the deck area of the requested playermat and returns the result as table
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getDeckAreaObjects = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.call("getDeckAreaObjects")
|
|
end
|
|
end
|
|
|
|
-- Flips the top card of the deck (useful after deck manipulation for Norman Withers)
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.flipTopCardFromDeck = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.call("flipTopCardFromDeck")
|
|
end
|
|
end
|
|
|
|
-- Returns the position of the discard pile of the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getDiscardPosition = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.call("returnGlobalDiscardPosition")
|
|
end
|
|
end
|
|
|
|
-- Returns the position of the draw pile of the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getDrawPosition = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.call("returnGlobalDrawPosition")
|
|
end
|
|
end
|
|
|
|
-- Transforms a local position into a global position
|
|
---@param localPos table Local position to be transformed
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.transformLocalPosition = function(localPos, matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.positionToWorld(localPos)
|
|
end
|
|
end
|
|
|
|
-- Returns the rotation of the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.returnRotation = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getRotation()
|
|
end
|
|
end
|
|
|
|
-- Returns a table with spawn data (position and rotation) for a helper object
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@param helperName string Name of the helper object
|
|
PlayermatApi.getHelperSpawnData = function(matColor, helperName)
|
|
local resultTable = {}
|
|
local localPositionTable = {
|
|
["Hand Helper"] = {0.05, 0, -1.182},
|
|
["Search Assistant"] = {-0.3, 0, -1.182}
|
|
}
|
|
|
|
for color, mat in pairs(getMatForColor(matColor)) do
|
|
resultTable[color] = {
|
|
position = mat.positionToWorld(localPositionTable[helperName]),
|
|
rotation = mat.getRotation()
|
|
}
|
|
end
|
|
return resultTable
|
|
end
|
|
|
|
|
|
-- Triggers the Upkeep for the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@param playerColor string Color of the calling player (for messages)
|
|
PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("doUpkeepFromHotkey", playerColor)
|
|
end
|
|
end
|
|
|
|
-- Handles discarding for the requested playermat for the provided list of objects
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
---@param objList table List of objects to discard
|
|
PlayermatApi.discardListOfObjects = function(matColor, objList)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("discardListOfObjects", objList)
|
|
end
|
|
end
|
|
|
|
-- Returns the active investigator id
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.returnInvestigatorId = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getVar("activeInvestigatorId")
|
|
end
|
|
end
|
|
|
|
-- Returns the class of the active investigator
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.returnInvestigatorClass = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.getVar("activeInvestigatorClass")
|
|
end
|
|
end
|
|
|
|
-- Returns the position for encounter card drawing
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
---@param stack boolean If true, returns the leftmost position instead of the first empty from the right
|
|
PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return Vector(mat.call("getEncounterCardDrawPosition", stack))
|
|
end
|
|
end
|
|
|
|
-- Sets the requested playermat's snap points to limit snapping to matching card types or not. If
|
|
-- matchTypes is true, the main card slot snap points will only snap assets, while the
|
|
-- investigator area point will only snap Investigators. If matchTypes is false, snap points will
|
|
-- be reset to snap all cards.
|
|
---@param matchCardTypes boolean Whether snap points should only snap for the matching card types
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("setLimitSnapsByType", matchCardTypes)
|
|
end
|
|
end
|
|
|
|
-- Sets the requested playermat's draw 1 button to visible
|
|
---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("showDrawButton", isDrawButtonVisible)
|
|
end
|
|
end
|
|
|
|
-- Shows or hides the clickable clue counter for the requested playermat
|
|
---@param showCounter boolean Whether the clickable counter should be present or not
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.clickableClues = function(showCounter, matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("clickableClues", showCounter)
|
|
end
|
|
end
|
|
|
|
-- Toggles the use of class textures for the requested playermat
|
|
---@param state boolean Whether the class texture should be used or not
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.useClassTexture = function(state, matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("useClassTexture", state)
|
|
end
|
|
end
|
|
|
|
-- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.removeClues = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("removeClues")
|
|
end
|
|
end
|
|
|
|
-- Reports the clue count for the requested playermat
|
|
---@param useClickableCounters boolean Controls which type of counter is getting checked
|
|
PlayermatApi.getClueCount = function(useClickableCounters, matColor)
|
|
local count = 0
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
count = count + mat.call("getClueCount", useClickableCounters)
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- Updates the specified owned counter
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@param type string Counter to target
|
|
---@param newValue number Value to set the counter to
|
|
---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier
|
|
PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier })
|
|
end
|
|
end
|
|
|
|
-- Triggers the draw function for the specified playermat
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@param number number Amount of cards to draw
|
|
PlayermatApi.drawCardsWithReshuffle = function(matColor, number)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("drawCardsWithReshuffle", number)
|
|
end
|
|
end
|
|
|
|
-- Returns the resource counter amount
|
|
---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All")
|
|
---@param type string Counter to target
|
|
PlayermatApi.getCounterValue = function(matColor, type)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
return mat.call("getCounterValue", type)
|
|
end
|
|
end
|
|
|
|
-- Returns a list of mat colors that have an investigator placed
|
|
PlayermatApi.getUsedMatColors = function()
|
|
local usedColors = {}
|
|
for matColor, mat in pairs(getMatForColor("All")) do
|
|
local searchPos = mat.positionToWorld(localInvestigatorPosition)
|
|
local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck")
|
|
if #searchResult > 0 then
|
|
table.insert(usedColors, matColor)
|
|
end
|
|
end
|
|
return usedColors
|
|
end
|
|
|
|
-- Returns investigator name
|
|
---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All")
|
|
PlayermatApi.getInvestigatorName = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
local searchPos = mat.positionToWorld(localInvestigatorPosition)
|
|
local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck")
|
|
if #searchResult == 1 then
|
|
return searchResult[1].getName()
|
|
end
|
|
end
|
|
return ""
|
|
end
|
|
|
|
-- Resets the specified skill tracker to "1, 1, 1, 1"
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.resetSkillTracker = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("resetSkillTracker")
|
|
end
|
|
end
|
|
|
|
-- Redraws the XML for the slot symbols based on the slotData table
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.redrawSlotSymbols = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("redrawSlotSymbols")
|
|
end
|
|
end
|
|
|
|
-- Finds all objects on the playermat and associated set aside zone and returns a table
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
---@param filter string Name of the filte function (see util/SearchLib)
|
|
PlayermatApi.searchAroundPlayermat = function(matColor, filter)
|
|
local objList = {}
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do
|
|
table.insert(objList, obj)
|
|
end
|
|
end
|
|
return objList
|
|
end
|
|
|
|
-- Discard a non-hidden card from the corresponding player's hand
|
|
---@param matColor string Color of the playermat - White, Orange, Green, Red or All
|
|
PlayermatApi.doDiscardOne = function(matColor)
|
|
for _, mat in pairs(getMatForColor(matColor)) do
|
|
mat.call("doDiscardOne")
|
|
end
|
|
end
|
|
|
|
-- Triggers the metadata sync for all playermats
|
|
PlayermatApi.syncAllCustomizableCards = function()
|
|
for _, mat in pairs(getMatForColor("All")) do
|
|
mat.call("syncAllCustomizableCards")
|
|
end
|
|
end
|
|
|
|
return PlayermatApi
|
|
end
|
|
end)
|
|
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local PlayAreaApi = {}
|
|
local guidReferenceApi = require("core/GUIDReferenceApi")
|
|
|
|
local function getPlayArea()
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea")
|
|
end
|
|
|
|
local function getInvestigatorCounter()
|
|
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter")
|
|
end
|
|
|
|
-- 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")
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
|
|
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
|
|
-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'
|
|
---@param playerColor string Color of the player requesting the shift for messages
|
|
PlayAreaApi.shiftContentsUp = function(playerColor)
|
|
getPlayArea().call("shiftContentsUp", playerColor)
|
|
end
|
|
|
|
PlayAreaApi.shiftContentsDown = function(playerColor)
|
|
getPlayArea().call("shiftContentsDown", playerColor)
|
|
end
|
|
|
|
PlayAreaApi.shiftContentsLeft = function(playerColor)
|
|
getPlayArea().call("shiftContentsLeft", playerColor)
|
|
end
|
|
|
|
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)
|
|
end
|
|
|
|
---@param color string Connection color to be used for location connections
|
|
PlayAreaApi.setConnectionColor = function(color)
|
|
getPlayArea().call("setConnectionColor", color)
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
|
|
-- 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
|
|
end
|
|
end)
|
|
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
require("arkhamdb/DeckImporter")
|
|
end)
|
|
__bundle_register("arkhamdb/ArkhamDb", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local allCardsBagApi = require("playercards/AllCardsBagApi")
|
|
local playAreaApi = require("core/PlayAreaApi")
|
|
|
|
local ArkhamDb = {}
|
|
local internal = {}
|
|
|
|
local tabooList = {}
|
|
local configuration
|
|
|
|
local RANDOM_WEAKNESS_ID = "01000"
|
|
|
|
---@class Request
|
|
local Request = {}
|
|
|
|
-- Sets up the ArkhamDb interface. Should be called from the parent object on load.
|
|
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
|
|
internal.maybePrint("Private deck ID " .. deckId .. " is not shared.", playerColor)
|
|
return false, "Private deck " .. deckId .. " is not shared"
|
|
end
|
|
|
|
local json = JSON.decode(internal.fixUtf16String(status.text))
|
|
|
|
if not json then
|
|
internal.maybePrint("Deck ID " .. deckId .. " not found.", playerColor)
|
|
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
|
|
|
|
if randomWeaknessAmount > 0 then
|
|
for i = 1, randomWeaknessAmount do
|
|
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
|
|
|
|
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/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
|
|
|
|
Spawner = { }
|
|
|
|
-- 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
|
|
|
|
local miniCards = { }
|
|
local standardCards = { }
|
|
local investigatorCards = { }
|
|
|
|
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)
|
|
end
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
|
|
Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)
|
|
if sort then
|
|
table.sort(cardList, Spawner.cardComparator)
|
|
end
|
|
|
|
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)
|
|
end
|
|
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
|
|
end
|
|
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
|
|
})
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
|
|
-- 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 }
|
|
end
|
|
|
|
return spawnObjectData({
|
|
data = deck,
|
|
position = pos,
|
|
rotation = rot,
|
|
callback_function = callback
|
|
})
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
|
|
-- 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
|
|
end
|
|
|
|
-- 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)
|
|
end
|
|
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
|
|
end
|
|
end
|
|
|
|
-- 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
|
|
end
|
|
end
|
|
end)
|
|
__bundle_register("playermat/Zones", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
-- Sets up and returns coordinates for all possible spawn zones. Because Lua assigns tables by reference
|
|
-- and there is no built-in function to copy a table this is relatively brute force.
|
|
--
|
|
-- Positions are all relative to the player mat, and most are consistent. The
|
|
-- exception are the SetAside# zones, which are placed to the left of the mat
|
|
-- for White/Green, and the right of the mat for Orange/Red.
|
|
--
|
|
-- Investigator: Investigator card area.
|
|
-- Minicard: Placement for the investigator's minicard, just above the player mat
|
|
-- Deck, Discard: Standard locations for the deck and discard piles.
|
|
-- Blank1: used for assets that start in play (e.g. Duke)
|
|
-- Tarot, Hand1, Hand2, Ally, Blank4, Accessory, Arcane1, Arcane2, Body: Asset slot positions
|
|
-- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat area slots, and Threat4 is the blank threat area slot.
|
|
-- SetAside[1-3]: Column closest to the player mat, with 1 at the top and 3 at the bottom.
|
|
-- SetAside[4-6]: Column farther away from the mat, with 4 at the top and 6 at the bottom.
|
|
-- SetAside1: Permanent cards
|
|
-- SetAside2: Bonded cards
|
|
-- SetAside3: Ancestral Knowledge / Underworld Market
|
|
-- SetAside4: Upgrade sheets for customizable cards
|
|
-- SetAside5: Hunch Deck for Joe Diamond
|
|
-- SetAside6: currently unused
|
|
do
|
|
local playermatApi = require("playermat/PlayermatApi")
|
|
local Zones = { }
|
|
|
|
local commonZones = {}
|
|
commonZones["Investigator"] = { -1.177, 0, 0.002 }
|
|
commonZones["Deck"] = { -1.82, 0, 0 }
|
|
commonZones["Discard"] = { -1.82, 0, 0.61 }
|
|
commonZones["Ally"] = { -0.615, 0, 0.024 }
|
|
commonZones["Body"] = { -0.630, 0, 0.553 }
|
|
commonZones["Hand1"] = { 0.215, 0, 0.042 }
|
|
commonZones["Hand2"] = { -0.180, 0, 0.037 }
|
|
commonZones["Arcane1"] = { 0.212, 0, 0.559 }
|
|
commonZones["Arcane2"] = { -0.171, 0, 0.557 }
|
|
commonZones["Tarot"] = { 0.602, 0, 0.033 }
|
|
commonZones["Accessory"] = { 0.602, 0, 0.555 }
|
|
commonZones["Blank1"] = { 1.758, 0, 0.040 }
|
|
commonZones["Blank2"] = { 1.754, 0, 0.563 }
|
|
commonZones["Blank3"] = { 1.371, 0, 0.038 }
|
|
commonZones["Blank4"] = { 1.371, 0, 0.558 }
|
|
commonZones["Blank5"] = { 0.98, 0, 0.035 }
|
|
commonZones["Blank6"] = { 0.977, 0, 0.556 }
|
|
commonZones["Threat1"] = { -0.911, 0, -0.625 }
|
|
commonZones["Threat2"] = { -0.454, 0, -0.625 }
|
|
commonZones["Threat3"] = { 0.002, 0, -0.625 }
|
|
commonZones["Threat4"] = { 0.459, 0, -0.625 }
|
|
|
|
local zoneData = {}
|
|
zoneData["White"] = {}
|
|
zoneData["White"]["Investigator"] = commonZones["Investigator"]
|
|
zoneData["White"]["Deck"] = commonZones["Deck"]
|
|
zoneData["White"]["Discard"] = commonZones["Discard"]
|
|
zoneData["White"]["Ally"] = commonZones["Ally"]
|
|
zoneData["White"]["Body"] = commonZones["Body"]
|
|
zoneData["White"]["Hand1"] = commonZones["Hand1"]
|
|
zoneData["White"]["Hand2"] = commonZones["Hand2"]
|
|
zoneData["White"]["Arcane1"] = commonZones["Arcane1"]
|
|
zoneData["White"]["Arcane2"] = commonZones["Arcane2"]
|
|
zoneData["White"]["Tarot"] = commonZones["Tarot"]
|
|
zoneData["White"]["Accessory"] = commonZones["Accessory"]
|
|
zoneData["White"]["Blank1"] = commonZones["Blank1"]
|
|
zoneData["White"]["Blank2"] = commonZones["Blank2"]
|
|
zoneData["White"]["Blank3"] = commonZones["Blank3"]
|
|
zoneData["White"]["Blank4"] = commonZones["Blank4"]
|
|
zoneData["White"]["Blank5"] = commonZones["Blank5"]
|
|
zoneData["White"]["Blank6"] = commonZones["Blank6"]
|
|
zoneData["White"]["Threat1"] = commonZones["Threat1"]
|
|
zoneData["White"]["Threat2"] = commonZones["Threat2"]
|
|
zoneData["White"]["Threat3"] = commonZones["Threat3"]
|
|
zoneData["White"]["Threat4"] = commonZones["Threat4"]
|
|
zoneData["White"]["Minicard"] = { -1, 0, -1.45 }
|
|
zoneData["White"]["SetAside1"] = { 2.35, 0, -0.520 }
|
|
zoneData["White"]["SetAside2"] = { 2.35, 0, 0.042 }
|
|
zoneData["White"]["SetAside3"] = { 2.35, 0, 0.605 }
|
|
zoneData["White"]["UnderSetAside3"] = { 2.50, 0, 0.805 }
|
|
zoneData["White"]["SetAside4"] = { 2.78, 0, -0.520 }
|
|
zoneData["White"]["SetAside5"] = { 2.78, 0, 0.042 }
|
|
zoneData["White"]["SetAside6"] = { 2.78, 0, 0.605 }
|
|
zoneData["White"]["UnderSetAside6"] = { 2.93, 0, 0.805 }
|
|
|
|
zoneData["Orange"] = {}
|
|
zoneData["Orange"]["Investigator"] = commonZones["Investigator"]
|
|
zoneData["Orange"]["Deck"] = commonZones["Deck"]
|
|
zoneData["Orange"]["Discard"] = commonZones["Discard"]
|
|
zoneData["Orange"]["Ally"] = commonZones["Ally"]
|
|
zoneData["Orange"]["Body"] = commonZones["Body"]
|
|
zoneData["Orange"]["Hand1"] = commonZones["Hand1"]
|
|
zoneData["Orange"]["Hand2"] = commonZones["Hand2"]
|
|
zoneData["Orange"]["Arcane1"] = commonZones["Arcane1"]
|
|
zoneData["Orange"]["Arcane2"] = commonZones["Arcane2"]
|
|
zoneData["Orange"]["Tarot"] = commonZones["Tarot"]
|
|
zoneData["Orange"]["Accessory"] = commonZones["Accessory"]
|
|
zoneData["Orange"]["Blank1"] = commonZones["Blank1"]
|
|
zoneData["Orange"]["Blank2"] = commonZones["Blank2"]
|
|
zoneData["Orange"]["Blank3"] = commonZones["Blank3"]
|
|
zoneData["Orange"]["Blank4"] = commonZones["Blank4"]
|
|
zoneData["Orange"]["Blank5"] = commonZones["Blank5"]
|
|
zoneData["Orange"]["Blank6"] = commonZones["Blank6"]
|
|
zoneData["Orange"]["Threat1"] = commonZones["Threat1"]
|
|
zoneData["Orange"]["Threat2"] = commonZones["Threat2"]
|
|
zoneData["Orange"]["Threat3"] = commonZones["Threat3"]
|
|
zoneData["Orange"]["Threat4"] = commonZones["Threat4"]
|
|
zoneData["Orange"]["Minicard"] = { 1, 0, -1.45 }
|
|
zoneData["Orange"]["SetAside1"] = { -2.35, 0, -0.520 }
|
|
zoneData["Orange"]["SetAside2"] = { -2.35, 0, 0.042}
|
|
zoneData["Orange"]["SetAside3"] = { -2.35, 0, 0.605 }
|
|
zoneData["Orange"]["UnderSetAside3"] = { -2.50, 0, 0.805 }
|
|
zoneData["Orange"]["SetAside4"] = { -2.78, 0, -0.520 }
|
|
zoneData["Orange"]["SetAside5"] = { -2.78, 0, 0.042 }
|
|
zoneData["Orange"]["SetAside6"] = { -2.78, 0, 0.605 }
|
|
zoneData["Orange"]["UnderSetAside6"] = { -2.93, 0, 0.805 }
|
|
|
|
-- Green positions are the same as White and Red the same as Orange
|
|
zoneData["Red"] = zoneData["Orange"]
|
|
zoneData["Green"] = zoneData["White"]
|
|
|
|
-- Gets the global position for the given zone on the specified player mat.
|
|
---@param playerColor string Color name of the player mat to get the zone position for (e.g. "Red")
|
|
---@param zoneName string Name of the zone to get the position for. See Zones object documentation for a list of valid zones.
|
|
---@return tts__Vector|nil: Global position table, or nil if an invalid player color or zone is specified
|
|
Zones.getZonePosition = function(playerColor, zoneName)
|
|
if (playerColor ~= "Red"
|
|
and playerColor ~= "Orange"
|
|
and playerColor ~= "White"
|
|
and playerColor ~= "Green") then
|
|
return nil
|
|
end
|
|
return playermatApi.transformLocalPosition(zoneData[playerColor][zoneName], playerColor)
|
|
end
|
|
|
|
-- Return the global rotation for a card on the given player mat, based on its zone.
|
|
---@param playerColor string Color name of the player mat to get the rotation for (e.g. "Red")
|
|
---@param zoneName string Name of the zone. See Zones object documentation for a list of valid zones.
|
|
---@return tts__Vector: Global rotation vector for the given card. This will include the
|
|
-- Y rotation to orient the card on the given player mat as well as a
|
|
-- Z rotation to place the card face up or face down.
|
|
Zones.getDefaultCardRotation = function(playerColor, zoneName)
|
|
local cardRotation = playermatApi.returnRotation(playerColor)
|
|
if zoneName == "Deck" then
|
|
cardRotation = cardRotation + Vector(0, 0, 180)
|
|
end
|
|
return cardRotation
|
|
end
|
|
|
|
return Zones
|
|
end
|
|
end)
|
|
__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules)
|
|
do
|
|
local SearchLib = {}
|
|
local filterFunctions = {
|
|
isCard = function(x) return x.type == "Card" end,
|
|
isDeck = function(x) return x.type == "Deck" end,
|
|
isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end,
|
|
isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end,
|
|
isTileOrToken = function(x) return x.type == "Tile" end,
|
|
isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end,
|
|
}
|
|
|
|
-- performs the actual search and returns a filtered list of object references
|
|
---@param pos tts__Vector Global position
|
|
---@param rot? tts__Vector Global rotation
|
|
---@param size table Size
|
|
---@param filter? string Name of the filter function
|
|
---@param direction? table Direction (positive is up)
|
|
---@param maxDistance? number Distance for the cast
|
|
local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)
|
|
local filterFunc
|
|
if filter then
|
|
filterFunc = filterFunctions[filter]
|
|
end
|
|
local searchResult = Physics.cast({
|
|
origin = pos,
|
|
direction = direction or { 0, 1, 0 },
|
|
orientation = rot or { 0, 0, 0 },
|
|
type = 3,
|
|
size = size,
|
|
max_distance = maxDistance or 0
|
|
})
|
|
|
|
-- filter the result for matching objects
|
|
local objList = {}
|
|
for _, v in ipairs(searchResult) do
|
|
if not filter or filterFunc(v.hit_object) then
|
|
table.insert(objList, v.hit_object)
|
|
end
|
|
end
|
|
return objList
|
|
end
|
|
|
|
-- searches the specified area
|
|
SearchLib.inArea = function(pos, rot, size, filter)
|
|
return returnSearchResult(pos, rot, size, filter)
|
|
end
|
|
|
|
-- searches the area on an object
|
|
SearchLib.onObject = function(obj, filter)
|
|
local pos = obj.getPosition()
|
|
local size = obj.getBounds().size:setAt("y", 1)
|
|
return returnSearchResult(pos, _, size, filter)
|
|
end
|
|
|
|
-- searches the specified position (a single point)
|
|
SearchLib.atPosition = function(pos, filter)
|
|
local size = { 0.1, 2, 0.1 }
|
|
return returnSearchResult(pos, _, size, filter)
|
|
end
|
|
|
|
-- searches below the specified position (downwards until y = 0)
|
|
SearchLib.belowPosition = function(pos, filter)
|
|
local size = { 0.1, 2, 0.1 }
|
|
local direction = { 0, -1, 0 }
|
|
local maxDistance = pos.y
|
|
return returnSearchResult(pos, _, size, filter, direction, maxDistance)
|
|
end
|
|
|
|
return SearchLib
|
|
end
|
|
end)
|
|
return __bundle_require("__root")
|