ah_sce_unpacked/unpacked/Custom_Tile ArkhamDB Deck I...

1310 lines
51 KiB
Plaintext

-- Generated by EmmyLua (https://github.com/EmmyLua)
-- Created by Whimsical.
-- DateTime: 2021-08-19 6:38 a.m.
-- updated by Chr1Z (2022-10-11)
---@type ArkhamImportConfiguration
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
local bondedList = { }
local customizationRowsWithFields = { }
-- inputMap maps from (our 1-indexes) customization row index to inputValue table index
-- The Raven Quill
customizationRowsWithFields["09042"] = { }
customizationRowsWithFields["09042"].inputCount = 2
customizationRowsWithFields["09042"].inputMap = { }
customizationRowsWithFields["09042"].inputMap[1] = 1
customizationRowsWithFields["09042"].inputMap[5] = 2
-- Friends in Low Places
customizationRowsWithFields["09060"] = { }
customizationRowsWithFields["09060"].inputCount = 2
customizationRowsWithFields["09060"].inputMap = { }
customizationRowsWithFields["09060"].inputMap[1] = 1
customizationRowsWithFields["09060"].inputMap[3] = 2
-- Living Ink
customizationRowsWithFields["09079"] = { }
customizationRowsWithFields["09079"].inputCount = 3
customizationRowsWithFields["09079"].inputMap = { }
customizationRowsWithFields["09079"].inputMap[1] = 1
customizationRowsWithFields["09079"].inputMap[5] = 2
customizationRowsWithFields["09079"].inputMap[6] = 3
-- Grizzled
customizationRowsWithFields["09101"] = { }
customizationRowsWithFields["09101"].inputCount = 3
customizationRowsWithFields["09101"].inputMap = { }
customizationRowsWithFields["09101"].inputMap[1] = 1
customizationRowsWithFields["09101"].inputMap[2] = 2
customizationRowsWithFields["09101"].inputMap[3] = 3
-- 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
-- Table values:
-- redDeck: Deck ID to load for the red player
-- orangeDeck: Deck ID to load for the orange player
-- whiteDeck: Deck ID to load for the white player
-- greenDeck: Deck ID to load for the green player
-- private: True to load a private deck, false to load a public deck
-- loadNewest: True if the most upgraded version of the deck should be loaded
-- investigators: True if investigator cards should be spawned
function getUiState()
return {
redDeck = redDeckId,
orangeDeck = orangeDeckId,
whiteDeck = whiteDeckId,
greenDeck = greenDeckId,
private = privateDeck,
loadNewest = loadNewestDeck,
investigators = loadInvestigators
}
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.private
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()
-- TODO: Make this use the configuration ID for the all cards bag
local allCardsBag = getObjectFromGUID("15bb07")
local indexReady = allCardsBag.call("isIndexReady")
if not indexReady then
broadcastToAll("Still loading player cards, please try again in a few seconds", { 0.9, 0.2, 0.2 })
return
end
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
-- 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.
-- BlankTop: used for assets that start in play (e.g. Duke)
-- Tarot, Hand1, Hand2, Ally, BlankBottom, 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
local playerMatGuids = {}
playerMatGuids["Red"] = "0840d5"
playerMatGuids["Orange"] = "bd0ff4"
playerMatGuids["White"] = "8b081b"
playerMatGuids["Green"] = "383d8b"
commonZones = {}
commonZones["Investigator"] = { -1.17702, 0, 0.00209 }
commonZones["Minicard"] = { -0.4668214, 0, -1.222326 }
commonZones["Deck"] = { -1.822724, 0, -0.02940192 }
commonZones["Discard"] = { -1.822451, 0, 0.6092291 }
commonZones["Ally"] = { -0.6157398, 0, 0.02435675 }
commonZones["Body"] = { -0.6306521, 0, 0.553170 }
commonZones["Hand1"] = { 0.2155387, 0, 0.04257287 }
commonZones["Hand2"] = { -0.1803701, 0, 0.03745948 }
commonZones["Arcane1"] = { 0.2124223, 0, 0.5596902 }
commonZones["Arcane2"] = { -0.1711275, 0, 0.5567944 }
commonZones["Tarot"] = { 0.6016169, 0, 0.03273106 }
commonZones["Accessory"] = { 0.6049907, 0, 0.5546234 }
commonZones["BlankTop"] = { 1.758446, 0, 0.03965336 }
commonZones["BlankBottom"] = { 1.754469, 0, 0.5634764 }
commonZones["Threat1"] = { -0.9116555, 0, -0.6446251 }
commonZones["Threat2"] = { -0.4544126, 0, -0.6428719 }
commonZones["Threat3"] = { 0.002246313, 0, -0.6430681 }
commonZones["Threat4"] = { 0.4590618, 0, -0.6432732 }
Zones = {}
Zones["White"] = {}
Zones["White"]["Investigator"] = commonZones["Investigator"]
Zones["White"]["Minicard"] = commonZones["Minicard"]
Zones["White"]["Deck"] = commonZones["Deck"]
Zones["White"]["Discard"] = commonZones["Discard"]
Zones["White"]["Ally"] = commonZones["Ally"]
Zones["White"]["Body"] = commonZones["Body"]
Zones["White"]["Hand1"] = commonZones["Hand1"]
Zones["White"]["Hand2"] = commonZones["Hand2"]
Zones["White"]["Arcane1"] = commonZones["Arcane1"]
Zones["White"]["Arcane2"] = commonZones["Arcane2"]
Zones["White"]["Tarot"] = commonZones["Tarot"]
Zones["White"]["Accessory"] = commonZones["Accessory"]
Zones["White"]["BlankTop"] = commonZones["BlankTop"]
Zones["White"]["BlankBottom"] = commonZones["BlankBottom"]
Zones["White"]["Threat1"] = commonZones["Threat1"]
Zones["White"]["Threat2"] = commonZones["Threat2"]
Zones["White"]["Threat3"] = commonZones["Threat3"]
Zones["White"]["Threat4"] = commonZones["Threat4"]
Zones["White"]["SetAside1"] = { 2.345893, 0, -0.520315 }
Zones["White"]["SetAside2"] = { 2.345893, 0, 0.042552 }
Zones["White"]["SetAside3"] = { 2.345893, 0, 0.605419 }
Zones["White"]["UnderSetAside3"] = { 2.495893, 0, 0.805419 }
Zones["White"]["SetAside4"] = { 2.775893, 0, -0.520315 }
Zones["White"]["SetAside5"] = { 2.775893, 0, 0.042552 }
Zones["White"]["SetAside6"] = { 2.775893, 0, 0.605419 }
Zones["White"]["UnderSetAside6"] = { 2.925893, 0, 0.805419 }
Zones["Orange"] = {}
Zones["Orange"]["Investigator"] = commonZones["Investigator"]
Zones["Orange"]["Minicard"] = commonZones["Minicard"]
Zones["Orange"]["Deck"] = commonZones["Deck"]
Zones["Orange"]["Discard"] = commonZones["Discard"]
Zones["Orange"]["Ally"] = commonZones["Ally"]
Zones["Orange"]["Body"] = commonZones["Body"]
Zones["Orange"]["Hand1"] = commonZones["Hand1"]
Zones["Orange"]["Hand2"] = commonZones["Hand2"]
Zones["Orange"]["Arcane1"] = commonZones["Arcane1"]
Zones["Orange"]["Arcane2"] = commonZones["Arcane2"]
Zones["Orange"]["Tarot"] = commonZones["Tarot"]
Zones["Orange"]["Accessory"] = commonZones["Accessory"]
Zones["Orange"]["BlankTop"] = commonZones["BlankTop"]
Zones["Orange"]["BlankBottom"] = commonZones["BlankBottom"]
Zones["Orange"]["Threat1"] = commonZones["Threat1"]
Zones["Orange"]["Threat2"] = commonZones["Threat2"]
Zones["Orange"]["Threat3"] = commonZones["Threat3"]
Zones["Orange"]["Threat4"] = commonZones["Threat4"]
Zones["Orange"]["SetAside1"] = { -2.350362, 0, -0.520315 }
Zones["Orange"]["SetAside2"] = { -2.350362, 0, 0.042552 }
Zones["Orange"]["SetAside3"] = { -2.350362, 0, 0.605419 }
Zones["Orange"]["UnderSetAside3"] = { -2.500362, 0, 0.80419 }
Zones["Orange"]["SetAside4"] = { -2.7803627, 0, -0.520315 }
Zones["Orange"]["SetAside5"] = { -2.7803627, 0, 0.042552 }
Zones["Orange"]["SetAside6"] = { -2.7803627, 0, 0.605419 }
Zones["Orange"]["UnderSetAside6"] = { -2.9303627, 0, 0.80419 }
-- Green positions are the same as White and Red the same as Orange
Zones["Red"] = Zones["Orange"]
Zones["Green"] = Zones["White"]
-- Returns the zone name where the specified card should be placed, based on its metadata.
---@param cardMetadata: Table of card metadata. Metadata fields type and permanent are required; all others are optional.
---@return: Zone name such as "Deck", "SetAside1", etc. See Zones object documentation for a list of valid zones.
function Zones.getDefaultCardZone(cardMetadata)
if cardMetadata.type == "Investigator" then
return "Investigator"
elseif cardMetadata.type == "Minicard" then
return "Minicard"
elseif cardMetadata.permanent then
return "SetAside1"
elseif bondedList[cardMetadata.id] then
return "SetAside2"
-- SetAside3 is used for Ancestral Knowledge / Underworld Market
-- SetAside4 is used for upgrade sheets
else
return "Deck"
end
end
-- Gets the global position for the given zone on the specified player mat.
---@param playerColor: Color name of the player mat to get the zone position for (e.g. "Red")
---@param zoneName: Name of the zone to get the position for. See Zones object documentation for a list of valid zones.
---@return: Global position table, or nil if an invalid player color or zone is specified
function Zones.getZonePosition(playerColor, zoneName)
if (playerColor ~= "Red"
and playerColor ~= "Orange"
and playerColor ~= "White"
and playerColor ~= "Green") then
return nil
end
return getObjectFromGUID(playerMatGuids[playerColor]).positionToWorld(Zones[playerColor][zoneName])
end
-- Return the global rotation for a card on the given player mat, based on its metadata.
---@param playerColor: Color name of the player mat to get the rotation for (e.g. "Red")
---@param cardMetadata: Table of card metadata. Metadata fields type and permanent are required; all others are optional.
---@return: 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.
function Zones.getDefaultCardRotation(playerColor, zone)
local deckRotation = getObjectFromGUID(playerMatGuids[playerColor]).getRotation()
if zone == "Investigator" then
deckRotation = deckRotation + Vector(0, 270, 0)
elseif zone == "Deck" then
deckRotation = deckRotation + Vector(0, 0, 180)
end
return deckRotation
end
local RANDOM_WEAKNESS_ID = "01000"
local tags = { configuration = "import_configuration_provider" }
local Priority = {
ERROR = 0,
WARNING = 1,
INFO = 2,
DEBUG = 3
}
---@type fun(text: string)
local printFunction = printToAll
local printPriority = Priority.INFO
---@param priority number
---@return string
function Priority.getLabel(priority)
if priority == 0 then return "ERROR"
elseif priority == 1 then return "WARNING"
elseif priority == 2 then return "INFO"
elseif priority == 3 then return "DEBUG"
else error(table.concat({ "Priority", priority, "not found" }, " ")) return ""
end
end
---@param message string
---@param priority number
local function debugPrint(message, priority, color)
if (color == nil) then
color = { 0.5, 0.5, 0.5 }
end
if (printPriority >= priority) then
printFunction("[" .. Priority.getLabel(priority) .. "] " .. message, color)
end
end
local function fixUtf16String(str)
return str:gsub("\\u(%w%w%w%w)", function(match)
return string.char(tonumber(match, 16))
end)
end
--Forward declaration
---@type Request
local Request = {}
---@type table<string, ArkhamImportTaboo>
local tabooList = {}
---@return ArkhamImportConfiguration
local function getConfiguration()
local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration")
printPriority = configuration.priority
return configuration
end
function onLoad(script_state)
local state = JSON.decode(script_state)
initializeUi(state)
math.randomseed(os.time())
local configuration = getConfiguration()
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
local json = JSON.decode(fixUtf16String(status.text))
for _, taboo in pairs(json) do
---@type <string, boolean>
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
function onSave() return JSON.encode(getUiState()) 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 ArkhamImportDeck
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param configuration ArkhamImportConfiguration
local function onDeckResult(deck, playerColor, configuration)
-- Load the next deck in the upgrade path if the option is enabled
if (getUiState().loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
buildDeck(playerColor, deck.next_deck)
return
end
debugPrint(table.concat({ "Found decklist: ", deck.name }), Priority.INFO, playerColor)
debugPrint(table.concat({ "-", deck.name, "-" }), Priority.DEBUG)
for k, v in pairs(deck) do
if type(v) == "table" then
debugPrint(table.concat { k, ": <table>" }, Priority.DEBUG)
else
debugPrint(table.concat { k, ": ", tostring(v) }, Priority.DEBUG)
end
end
debugPrint("", Priority.DEBUG)
-- 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
maybeDrawRandomWeakness(slots, playerColor, configuration)
maybeAddInvestigatorCards(deck, slots)
extractBondedCards(slots, configuration)
checkTaboos(deck.taboo_id, slots, playerColor, configuration)
local commandManager = getObjectFromGUID(configuration.command_manager_guid)
---@type ArkhamImport_CommandManager_InitializationArguments
local parameters = {
configuration = configuration,
description = deck.description_md,
}
---@type ArkhamImport_CommandManager_InitializationResults
local results = commandManager:call("initialize", parameters)
if not results.is_successful then
debugPrint(results.error_message, Priority.ERROR)
return
end
-- get upgrades for customizable cards
local meta = deck.meta
local customizations = {}
if meta then customizations = JSON.decode(deck.meta) end
loadCards(slots, deck.investigator_code, playerColor, commandManager,
configuration, results.configuration, customizations)
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: 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: Color name of the player this deck is being loaded for. Used for broadcast if a weakness is added.
---@param configuration: The API configuration object
function maybeDrawRandomWeakness(slots, playerColor, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local hasRandomWeakness = false
for cardId, cardCount in pairs(slots) do
if cardId == RANDOM_WEAKNESS_ID then
hasRandomWeakness = true
break
end
end
if hasRandomWeakness then
local weaknessId = allCardsBag.call("getRandomWeaknessId")
slots[weaknessId] = 1
slots[RANDOM_WEAKNESS_ID] = nil
debugPrint("Random basic weakness added to deck", Priority.INFO, playerColor)
end
end
-- If investigator cards should be loaded, add both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
---@param deck: The processed ArkhamDB deck response
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function maybeAddInvestigatorCards(deck, slots)
if getUiState().investigators then
local investigatorId = deck.investigator_code
slots[investigatorId .. "-m"] = 1
local deckMeta = JSON.decode(deck.meta)
local parallelFront = deckMeta ~= nil and deckMeta.alternate_front ~= nil and deckMeta.alternate_front ~= ""
local parallelBack = deckMeta ~= nil and deckMeta.alternate_back ~= nil and deckMeta.alternate_back ~= ""
if parallelFront and parallelBack then
investigatorId = investigatorId .. "-p"
elseif parallelFront then
local alternateNum = tonumber(deckMeta.alternate_front)
if alternateNum >= 01501 and alternateNum <= 01506 then
investigatorId = investigatorId .. "-r"
else
investigatorId = investigatorId .. "-pf"
end
elseif parallelBack then
investigatorId = investigatorId .. "-pb"
end
slots[investigatorId] = 1
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: 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 configuration: The API configuration object
function extractBondedCards(slots, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
-- Create a list of bonded cards first so we don't modify slots while iterating
local bondedCards = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil and card.metadata.bonded ~= nil) then
for _, bond in ipairs(card.metadata.bonded) do
bondedCards[bond.id] = bond.count
-- We need to know which cards are bonded to determine their position, remember them
bondedList[bond.id] = true
end
end
end
-- Add any bonded cards to the main slots list
for bondedId, bondedCount in pairs(bondedCards) do
slots[bondedId] = bondedCount
end
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: 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: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function checkTaboos(tabooId, slots, playerColor, configuration)
if tabooId then
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
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 = allCardsBag.call("getCardById", { id = cardId .. "-t" })
if tabooCard == nil then
local basicCard = allCardsBag.call("getCardById", { id = cardId })
debugPrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version",
Priority.WARNING, playerColor)
else
slots[cardId .. "-t"] = slots[cardId]
slots[cardId] = nil
end
end
end
end
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 method uses an encapsulated coroutine with yields to make the card spawning cleaner.
--
---@param slots: Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn, and count is the number which should be spawned
---@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 playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param configuration: Loader configuration object
---@param customizations: ArkhamDB data for customizations on customizable cards
function loadCards(slots, investigatorId, playerColor, commandManager, configuration, command_config, customizations)
function coinside()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local yPos = {}
local cardsToSpawn = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if card ~= nil then
local cardZone = Zones.getDefaultCardZone(card.metadata)
for i = 1, cardCount do
table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
end
-- upgrade sheets for customizable cards
local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" })
if upgradesheet ~= nil then
-- update metadata for spawned upgrade sheets
local upgrades = customizations["cus_" .. cardId]
if upgrades ~= nil then
-- initialize tables
-- markedBoxes: contains the amount of markedBoxes (left to right) per row (starting at row 1)
-- inputValues: contains the amount of inputValues per row (starting at row 0)
local markedBoxes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
local inputValues = {}
local index_xp = {}
-- get the index and xp values (looks like this: X|X,X|X, ..)
for str in string.gmatch(customizations["cus_" .. cardId], "([^,]+)") do
table.insert(index_xp, str)
end
-- split each pair and assign it to the proper position in markedBoxes
if (customizationRowsWithFields[cardId] ~= nil) then
for i = 1, customizationRowsWithFields[cardId].inputCount do
table.insert(inputValues, "")
end
end
local inputCount = 0
for _, entry in ipairs(index_xp) do
local counter = 0
local index = 0
-- if found number is 0, then only get inputvalue
for str in string.gmatch(entry, "([^|]+)") do
counter = counter + 1
if counter == 1 then
index = tonumber(str) + 1
elseif counter == 2 then
markedBoxes[index] = tonumber(str)
elseif counter == 3 and str ~= "" then
if (cardId == "09042") then
inputValues[customizationRowsWithFields[cardId].inputMap[index]] =
convertRavenQuillSelections(str)
else
inputValues[customizationRowsWithFields[cardId].inputMap[index]] = str
end
end
end
end
-- remove first entry in markedBoxes if row 0 has textbox
if customizationRowsWithFields[cardId] ~= nil
and customizationRowsWithFields[cardId].inputCount > 0 then
table.remove(markedBoxes, 1)
end
-- write the loaded values to the save_data of the sheets
upgradesheet.data["LuaScriptState"] = JSON.encode({ markedBoxes, inputValues })
table.insert(cardsToSpawn, { data = upgradesheet.data, metadata = upgradesheet.metadata, zone = "SetAside4" })
end
end
-- spawn additional minicard for 'Summoned Servitor'
if cardId == "09080" then
local servitor = allCardsBag.call("getCardById", { id = "09080-m" })
table.insert(cardsToSpawn, { data = servitor.data, metadata = servitor.metadata, zone = "SetAside6" })
end
slots[cardId] = 0
end
end
-- TODO: Re-enable this later, as a command
-- handleAltInvestigatorCard(cardsToSpawn, "promo", configuration)
table.sort(cardsToSpawn, cardComparator)
-- TODO: Process commands for the cardsToSpawn list
-- These should probably be commands, once the command handler is updated
handleStartsInPlay(cardsToSpawn)
handleAncestralKnowledge(cardsToSpawn)
handleUnderworldMarket(cardsToSpawn, playerColor)
handleHunchDeck(investigatorId, cardsToSpawn, playerColor)
-- Count the number of cards in each zone so we know if it's a deck or card.
-- TTS's Card vs. Deck distinction requires this since we can't spawn a deck with only one card
local zoneCounts = getZoneCounts(cardsToSpawn)
local zoneDecks = {}
for zone, count in pairs(zoneCounts) do
if count > 1 then
zoneDecks[zone] = buildDeckDataTemplate()
end
end
-- For each card in a deck zone, add it to that deck. Otherwise, spawn it directly
for _, spawnCard in ipairs(cardsToSpawn) do
if zoneDecks[spawnCard.zone] ~= nil then
addCardToDeck(zoneDecks[spawnCard.zone], spawnCard.data)
else
local cardPos = Zones.getZonePosition(playerColor, spawnCard.zone)
cardPos.y = 2
spawnObjectData({
data = spawnCard.data,
position = cardPos,
rotation = Zones.getDefaultCardRotation(playerColor, spawnCard.zone),
})
end
end
-- Spawn each of the decks
for zone, deck in pairs(zoneDecks) do
local deckPos = Zones.getZonePosition(playerColor, zone)
deckPos.y = 3
local spreadCallback = nil;
if (zone == "SetAside4") then
-- SetAside4 is reserved for customization cards, and we want them spread on the table
-- so their checkboxes are visible
if (playerColor == "White") then
deckPos.z = deckPos.z + (#deck.ContainedObjects - 1)
elseif (playerColor == "Green") then
deckPos.x = deckPos.x + (#deck.ContainedObjects - 1)
end
spreadCallback = function(deck) deck.spread(1.0) end
end
spawnObjectData({
data = deck,
position = deckPos,
rotation = Zones.getDefaultCardRotation(playerColor, zone),
callback_function = spreadCallback
})
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
local request = Request.start({
configuration.api_uri,
configuration.cards,
cardId
},
function(result)
local adbCardInfo = JSON.decode(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
debugPrint("Card not found: " .. cardName .. ", ArkhamDB ID " .. cardId, Priority.ERROR, playerColor)
else
debugPrint("Card not found in ArkhamDB, ID " .. cardId, Priority.ERROR, playerColor)
end
end)
end
end
if (not hadError) then
debugPrint("Deck loaded successfully!", Priority.INFO, playerColor)
end
return 1
end
startLuaCoroutine(self, "coinside")
end
-- Conver 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 An ArkhamDB string indicating the customization selections for The Raven's Quill. 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
-- 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 configuration = getConfiguration()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil) 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
-- 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: TTS deck data structure to add to
---@param card: Data for the card to be inserted
function addCardToDeck(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 = 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
function findNextAvailableId(objectTable, startId)
local id = startId
while (objectTable[id] ~= nil) do
id = tostring(tonumber(id) + 1)
end
return id
end
-- Count the number of cards in each zone
---@param cards: Table of {cardData, cardMetadata, zone}
---@return: Table of {zoneName=zoneCount}
function getZoneCounts(cards)
local counts = {}
for _, card in ipairs(cards) do
if counts[card.zone] == nil then
counts[card.zone] = 1
else
counts[card.zone] = counts[card.zone] + 1
end
end
return counts
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 containing the minimal TTS deck data structure
function buildDeckDataTemplate()
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
deck.Transform = {
scaleX = 1,
scaleY = 1,
scaleZ = 1,
}
return deck
end
-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.
---@return: 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.
function getpbcn(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.
function cardComparator(card1, card2)
local pbcn1 = getpbcn(card1.metadata)
local pbcn2 = 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
-- Replace the investigator card and minicard with an alternate version. This
-- will find the relevant cards and look for IDs with <id>-<altVersionTag>, and
-- <id>-<altVersionTag>-m, and update the entries in cardList with the new card
-- data.
--
---@param cardList: Deck list being created
---@param altVersionTag: The tag for the different version, currently the only alt versions are "promo", but will soon inclide "revised"
---@param configuration: ArkhamDB configuration defniition, used for the card bag
function handleAltInvestigatorCard(cardList, altVersionTag, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for _, card in ipairs(cardList) do
if card.metadata.type == "Investigator" then
local altInvestigator = allCardsBag.call("getCardById", { id = card.metadata.id .. "-" .. altVersionTag })
if (altInvestigator ~= nil) then
card.data = altInvestigator.data
card.metadata = altInvestigator.metadata
end
end
if card.metadata.type == "Minicard" then
-- -promo comes before -m in the ID, so needs a little massaging
local investigatorId = string.sub(card.metadata.id, 1, 5)
local altMinicard = allCardsBag.call("getCardById", { id = investigatorId .. "-" .. altVersionTag .. "-m" })
if altMinicard ~= nil then
card.data = altMinicard.data
card.metadata = altMinicard.metadata
end
end
end
end
-- Place cards which start in play (Duke, Sophie) in the play area
function handleStartsInPlay(cardList)
for _, card in ipairs(cardList) do
if card.metadata.startsInPlay then card.zone = "BlankTop" end
end
end
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3
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.metadata.bonded_to == nil
and not card.metadata.weakness) then
table.insert(skillList, i)
end
end
if hasAncestralKnowledge then
for i = 1, 5 do
-- Move 5 random skills to SetAside3
local skillListIndex = math.random(#skillList)
cardList[skillList[skillListIndex]].zone = "UnderSetAside3"
table.remove(skillList, skillListIndex)
end
end
end
-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3
---@param cardList: Deck list being created
---@param playerColor: 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 skills, doing both in one pass
for i, card in ipairs(cardList) do
if card.metadata.id == "09077" then
-- Underworld Market found
hasMarket = true
card.zone = "SetAside3"
elseif (string.find(card.metadata.traits or "", "Illicit", 1, true)
and card.metadata.bonded_to == nil
and not card.metadata.weakness) then
table.insert(illicitList, i)
end
end
if hasMarket then
if #illicitList < 10 then
debugPrint("Only " .. #illicitList .. " Illicit cards in your deck, you can't trigger Underworld Market's ability."
, Priority.WARNING, 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
debugPrint("Moved all " .. #illicitList .. " Illicit cards to the Market deck, reduce it to 10", Priority.INFO,
playerColor)
else
debugPrint("Built the Market deck", Priority.INFO, playerColor)
end
end
end
end
-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck.
---@param investigatorId: ID for the deck's investigator card. Passed separately because the investigator may not be included in the cardList
---@param cardList: Deck list being created
---@param playerColor: Color this deck is being loaded for
function handleHunchDeck(investigatorId, cardList, playerColor)
if investigatorId == "05002" then -- Joe Diamond
local insightList = {}
for i, card in ipairs(cardList) do
if (card.metadata.type == "Event"
and string.match(card.metadata.traits or "", "Insight")
and card.metadata.bonded_to == nil) then
table.insert(insightList, i)
end
end
-- Process insights 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
debugPrint("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList .. " Insight events.",
Priority.INFO, playerColor)
elseif #insightList > 11 then
debugPrint("Moved all " .. #insightList .. " Insight events to the hunch deck, reduce it to 11.", Priority.INFO,
playerColor)
else
debugPrint("Built Joe's hunch deck", Priority.INFO, playerColor)
end
end
end
-- Test method. Loads all decks which were submitted to ArkhamDB on a given date window.
function testLoadLotsOfDecks()
local configuration = getConfiguration()
local numDays = 7
local day = os.time { year = 2021, month = 7, day = 15 } -- Start date here
for i = 1, numDays do
local dateString = os.date("%Y-%m-%d", day)
local deckList = Request.start({
configuration.api_uri,
"decklists/by_date",
dateString,
},
function(result)
local json = JSON.decode(result.text)
for i, deckData in ipairs(json) do
buildDeck(getColorForTest(i), deckData.id)
end
end)
day = day + (60 * 60 * 24) -- Move forward by one day
end
end
-- Rotates the player mat based on index, to spread the card stacks during a mass load
function getColorForTest(index)
if (index % 4 == 0) then
return "Red"
elseif (index % 4 == 1) then
return "Orange"
elseif (index % 4 == 2) then
return "White"
elseif (index % 4 == 3) then
return "Green"
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: ArkhamDB deck id to be loaded
function buildDeck(playerColor, deckId)
local configuration = getConfiguration()
-- 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 allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local checkCard = allCardsBag.call("getCardById", { id = "01001" })
if (checkCard ~= nil and checkCard.data == nil) then
return
end
local deckUri = { configuration.api_uri,
getUiState().private and configuration.private_deck or configuration.public_deck, deckId }
local deck = Request.start(deckUri, function(status)
if string.find(status.text, "<!DOCTYPE html>") then
debugPrint("Private deck ID " .. deckId .. " is not shared", Priority.ERROR, playerColor)
return false, table.concat({ "Private deck ", deckId, " is not shared" })
end
local json = JSON.decode(status.text)
if not json then
debugPrint("Deck ID " .. deckId .. " not found", Priority.ERROR, playerColor)
return false, "Deck not found!"
end
return true, JSON.decode(status.text)
end)
deck:with(onDeckResult, playerColor, configuration)
end
---@type Request
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 string
---@param configure fun(request: Request, status: WebRequestStatus)
---@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 string
---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
---@param on_error fun(status: WebRequestStatus)|nil
---@vararg any[]
---@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 weather the resultant data is as expected, and the processed content of the request.
---@param uri string
---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
---@vararg any[]
---@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
---@vararg any
function Request.with_all(requests, on_success, on_error, ...)
local parameters = table.pack(...)
Wait.condition(function()
---@type any[]
local results = {}
---@type Request[]
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
debugPrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }), Priority.ERROR)
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
---@param callback fun(content: any, vararg any)
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