--- --- Generated by EmmyLua(https://github.com/EmmyLua) --- Created by Whimsical. --- DateTime: 2021-08-19 6:38 a.m. --- ---@type ArkhamImportConfiguration -- Begin LoaderUi included file 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 loadingColor = "" -- 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 else redDeckId = "" orangeDeckId = "" whiteDeckId = "" greenDeckId = "" privateDeck = true loadNewestDeck = true loadInvestigators = true end makeOptionToggles() makeDeckIdFields() makeBuildButton() end function makeOptionToggles() -- Creates the three option toggle buttons. Each toggle assumes its index as -- part of the toggle logic. IF YOU CHANGE THE ORDER OF THESE FIELDS YOU MUST -- CHANGE THE EVENT HANDLERS makePublicPrivateToggle() makeLoadUpgradedToggle() makeLoadInvestigatorsToggle() end function makePublicPrivateToggle() local checkbox_parameters = {} checkbox_parameters.click_function = "publicPrivateChanged" checkbox_parameters.function_owner = self checkbox_parameters.position = {0.25,0.1,-0.102} checkbox_parameters.width = INPUT_FIELD_WIDTH checkbox_parameters.height = INPUT_FIELD_HEIGHT checkbox_parameters.tooltip = "Published or private deck.\n\n*****PLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!" checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck] checkbox_parameters.font_size = 240 checkbox_parameters.scale = {0.1,0.1,0.1} checkbox_parameters.color = FIELD_COLOR checkbox_parameters.hover_color = {0.4,0.6,0.8} self.createButton(checkbox_parameters) end function makeLoadUpgradedToggle() local checkbox_parameters = {} checkbox_parameters.click_function = "loadUpgradedChanged" checkbox_parameters.function_owner = self checkbox_parameters.position = {0.25,0.1,-0.01} checkbox_parameters.width = INPUT_FIELD_WIDTH checkbox_parameters.height = INPUT_FIELD_HEIGHT checkbox_parameters.tooltip = "Load newest upgrade, or exact deck" checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] checkbox_parameters.font_size = 240 checkbox_parameters.scale = {0.1,0.1,0.1} checkbox_parameters.color = FIELD_COLOR checkbox_parameters.hover_color = {0.4,0.6,0.8} self.createButton(checkbox_parameters) end function makeLoadInvestigatorsToggle() local checkbox_parameters = {} checkbox_parameters.click_function = "loadInvestigatorsChanged" checkbox_parameters.function_owner = self checkbox_parameters.position = {0.25,0.1,0.081} checkbox_parameters.width = INPUT_FIELD_WIDTH checkbox_parameters.height = INPUT_FIELD_HEIGHT checkbox_parameters.tooltip = "Spawn investigator cards?" checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] checkbox_parameters.font_size = 240 checkbox_parameters.scale = {0.1,0.1,0.1} checkbox_parameters.color = FIELD_COLOR checkbox_parameters.hover_color = {0.4,0.6,0.8} 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 handler for the Public/Private toggle. Changes the local value and the -- labels to toggle the button function publicPrivateChanged() -- editButton uses parameters.index which is 0-indexed privateDeck = not privateDeck self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck], } end -- Event handler for the Upgraded toggle. Changes the local value and the -- labels to toggle the button function loadUpgradedChanged() -- editButton uses parameters.index which is 0-indexed loadNewestDeck = not loadNewestDeck self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck], } end -- Event handler for the load investigator cards toggle. Changes the local -- value and the labels to toggle the button function loadInvestigatorsChanged() -- editButton uses parameters.index which is 0-indexed loadInvestigators = not loadInvestigators self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators], } end -- Event handler for deck ID change function redDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) redDeckId = inputValue end -- Event handler for deck ID change function orangeDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) orangeDeckId = inputValue end -- Event handler for deck ID change function whiteDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) whiteDeckId = inputValue end -- Event handler for deck ID change function greenDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) greenDeckId = inputValue end function loadDecks() -- testLoadLotsOfDecks() -- Method in DeckImporterMain, visible due to inclusion -- 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 -- End LoaderUi included file -- Begin Zones included file (with minor modifications) -- 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. -- -- Valid Zones: -- Investigator: Investigator card area. -- Minicard: Placement for the investigator's minicard. This is just above the -- player mat, vertically in line with the investigator card area. -- Deck, Discard: Standard locations for the deck and discard piles. -- BlankTop, Tarot, Hand1, Hand2, Ally, BlankBottom, Accessory, Arcane1, -- Arcane2, Body: Asset slot positions on the player mat. -- 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-6]: Areas outside the player mat, to the right for Red/Orange and -- the left for White/Green. SetAside[1-3] are a column closest to the -- player mat, with 1 at the top of the mat and 3 at the bottom. -- SetAside[4-6] are a column farther away from the mat, with 4 at the top -- and 6 at the bottom. local playerMatGuids = { } playerMatGuids["Red"] = "0840d5" playerMatGuids["Orange"] = "bd0ff4" playerMatGuids["White"] = "8b081b" playerMatGuids["Green"] = "383d8b" local Zones = { } local commonZones = { } commonZones["Investigator"] = {-0.7833852, 0, 0.0001343352} commonZones["Minicard"] = {-0.7833852, 0, -1.187242} commonZones["Deck"] = {-1.414127, 0, -0.006668129} commonZones["Discard"] = {-1.422189,0,0.643440} commonZones["Ally"] = {-0.236577,0,0.023543} commonZones["Body"] = {-0.257249,0,0.553170} commonZones["Hand1"] = {0.600493,0,0.037291} commonZones["Hand2"] = {0.206867,0,0.025540} commonZones["Arcane1"] = {0.585817,0,0.567969} commonZones["Arcane2"] = {0.197267,0,0.562296} commonZones["Tarot"] = {0.980616,0,0.047756} commonZones["Accessory"] = {0.976689,0,0.569344} commonZones["BlankTop"] = {1.364696,0,0.062552} commonZones["BlankBottom"] = {1.349999,0,0.585419} commonZones["Threat1"] = {-0.835423,0,-0.633271} commonZones["Threat2"] = {-0.384615,0,-0.633493} commonZones["Threat3"] = {0.071090,0,-0.633717} commonZones["Threat4"] = {0.520816,0,-0.633936} 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.004500,0,-0.520315} Zones["White"]["SetAside2"] = {2.004500,0,0.042552} Zones["White"]["SetAside3"] = {2.004500,0,0.605419} Zones["White"]["UnderSetAside3"] = {2.154500,0,0.805419} Zones["White"]["SetAside4"] = {2.434500,0,-0.520315} Zones["White"]["SetAside5"] = {2.434500,0,0.042552} Zones["White"]["SetAside6"] = {2.434500,0,0.605419} Zones["White"]["UnderSetAside6"] = {2.584500,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.004500,0,-0.520315} Zones["Orange"]["SetAside2"] = {-2.004500,0,0.042552} Zones["Orange"]["SetAside3"] = {-2.004500,0,0.605419} Zones["Orange"]["UnderSetAside3"] = {-2.154500,0,0.80419} Zones["Orange"]["SetAside4"] = {-2.434500,0,-0.520315} Zones["Orange"]["SetAside5"] = {-2.434500,0,0.042552} Zones["Orange"]["SetAside6"] = {-2.434500,0,0.605419} Zones["Orange"]["UnderSetAside6"] = {-2.584500,0,0.80419} -- Green positions are the same as White, and Red the same as orange, so we -- can just point these at the White/Orange definitions 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 (cardMetadata.bonded_to ~= nil) then return "SetAside2" 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 ---@param str string ---@return string 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 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 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, ": "}, 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 loadCards(slots, playerColor, commandManager, configuration, results.configuration) 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 the UI indicates that 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 parallelFront = deck.meta ~= nil and deck.meta.alternate_front ~= nil and deck.meta.alternate_front ~= "" local parallelBack = deck.meta ~= nil and deck.meta.alternate_back ~= nil and deck.meta.alternate_back ~= "" if (parallelFront and parallelBack) then investigatorId = investigatorId.."-p" elseif (parallelFront) then investigatorId = investigatorId.."-pf" 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 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 any 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 playerColor String Color name of the player mat to place this deck -- on (e.g. "Red") -- Param commandManager -- Param configuration: Loader configuration object -- Param command_config: function loadCards(slots, playerColor, commandManager, configuration, command_config) 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 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) -- 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 spawnObjectData({ data = deck, position = deckPos, rotation = Zones.getDefaultCardRotation(playerColor, zone)}) 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 -- 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) table.insert(deck.ContainedObjects, cardData) table.insert(deck.DeckIDs, cardData.CardID) for customDeckId, customDeckData in pairs(cardData.CustomDeck) do deck.CustomDeck[customDeckId] = customDeckData end 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 PBN (Permanent/Bonded/Normal) value from the given metadata. -- Return: 1 for Permanent, 2 for Bonded, or 3 for Normal. The actual values -- are irrelevant as they provide only grouping and the order between them -- doesn't matter. function getPbn(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 -- 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 pbn1 = getPbn(card1.metadata) local pbn2 = getPbn(card2.metadata) if (pbn1 ~= pbn2) then return pbn1 > pbn2 end if (pbn1 == 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 -, and -- --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 -- 02014 = Duke (Ashcan Pete) -- 03009 = Sophie (Mark Harrigan) if (card.metadata.id == "02014" or card.metadata.id == "03009") 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 -- Ancestral Knowledge found 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 -- 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, "") 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