-- 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("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("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/CardSearch") end) __bundle_register("playercards/CardSearch", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/PlayerCardSpawner") local allCardsBagApi = require("playercards/AllCardsBagApi") local BUTTON_LABELS = {} BUTTON_LABELS["spawn"] = {} BUTTON_LABELS["spawn"][true] = "All matching cards" BUTTON_LABELS["spawn"][false] = "First matching card" BUTTON_LABELS["search"] = {} BUTTON_LABELS["search"][true] = "Name equals search term" BUTTON_LABELS["search"][false] = "Name contains search term" local inputParameters = {} inputParameters.label = "Click to enter card name" inputParameters.input_function = "input_func" inputParameters.function_owner = self inputParameters.alignment = 2 inputParameters.position = { x = 0, y = 0.1, z = -0.62 } inputParameters.width = 3750 inputParameters.height = 380 inputParameters.font_size = 350 inputParameters.scale = { 0.1, 1, 0.1 } inputParameters.color = { 0.9, 0.7, 0.5 } inputParameters.font_color = { 0, 0, 0 } function onSave() return JSON.encode({ spawnAll, searchExact, inputParameters.value }) end function onLoad(savedData) local loadedData = JSON.decode(savedData) spawnAll = loadedData[1] or false searchExact = loadedData[2] or false inputParameters.value = loadedData[3] or "" self.createInput(inputParameters) -- shared parameters local buttonParameters = {} buttonParameters.function_owner = self buttonParameters.font_size = 180 buttonParameters.scale = { 0.1, 1, 0.1 } buttonParameters.hover_color = { 0.4, 0.6, 0.8 } buttonParameters.color = { 0.9, 0.7, 0.5 } -- index 0: button for spawn mode buttonParameters.click_function = "toggleSpawnMode" buttonParameters.label = BUTTON_LABELS["spawn"][spawnAll] buttonParameters.position = { x = 0.16, y = 0.1, z = 0.565 } buttonParameters.height = 375 buttonParameters.width = 2300 self.createButton(buttonParameters) -- index 1: button for search mode buttonParameters.click_function = "toggleSearchMode" buttonParameters.label = BUTTON_LABELS["search"][searchExact] buttonParameters.position = { x = 0.16, y = 0.1, z = 0.652 } self.createButton(buttonParameters) -- index 2: start search buttonParameters.click_function = "startSearch" buttonParameters.label = "" buttonParameters.position = { x = 0, y = 0, z = 0.806 } buttonParameters.height = 600 buttonParameters.width = 2800 self.createButton(buttonParameters) end function toggleSpawnMode() spawnAll = not spawnAll self.editButton({ index = 0, label = BUTTON_LABELS["spawn"][spawnAll] }) end function toggleSearchMode() searchExact = not searchExact self.editButton({ index = 1, label = BUTTON_LABELS["search"][searchExact] }) end -- if "Enter press" (\n) is found, start search and recreate input function input_func(_, _, input, stillEditing) if not stillEditing then inputParameters.value = input elseif string.find(input, "%\n") ~= nil then inputParameters.value = input.gsub(input, "%\n", "") startSearch() self.removeInput(0) self.createInput(inputParameters) end end function startSearch() if inputParameters.value == nil or string.len(inputParameters.value) == 0 then printToAll("Please enter a search string.", "Yellow") return end if string.len(inputParameters.value) < 3 then printToAll("Please enter a longer search string.", "Yellow") return end if not allCardsBagApi.isBagPresent() then printToAll("Player card bag couldn't be found.", "Red") return end -- search all objects in bag local cardList = allCardsBagApi.getCardsByName(inputParameters.value, searchExact) if cardList == nil or #cardList == 0 then printToAll("No match found.", "Red") return end if (#cardList > 100) then printToAll("Matched more than 100 cards, please try a more specific search.", "Yellow") return end -- sort table by name (reverse for multiple results, because bottom card spawns first) table.sort(cardList, function(k1, k2) return spawnAll == (k1.data.Nickname > k2.data.Nickname) end) local rot = self.getRotation() local pos = self.positionToWorld(Vector(0, 2, -0.08)) Spawner.spawnCards(cardList, pos, rot, true) 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 -- Returns a specific card from the bag, based on ArkhamDB ID ---@param id table String ID of the card to retrieve ---@return table table -- If the indexes are still being constructed, an empty table is -- returned. Otherwise, a single table with the following fields -- cardData: TTS object data, suitable for spawning the card -- cardMetadata: 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() return 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 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 -- cardData: TTS object data, suitable for spawning the card -- cardMetadata: Table of parsed metadata AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) return getAllCardsBag().call("getCardsByClassAndLevel", {class = class, upgraded = upgraded}) end AllCardsBagApi.getCardsByCycle = function(cycle) return getAllCardsBag().call("getCardsByCycle", cycle) end AllCardsBagApi.getUniqueWeaknesses = function() return getAllCardsBag().call("getUniqueWeaknesses") end return AllCardsBagApi 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 table.insert(miniCards, card) else table.insert(standardCards, card) end end -- Spawn each of the three types individually. Each Y position shift 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 spawnObjectData({ data = cardList[1].data, position = pos, rotation = rot, callback_function = callback }) return end -- For multiple cards, construct a deck and spawn that local deck = Spawner.buildDeckDataTemplate() -- 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 = cardList[1].data.Transform.scaleX, scaleY = 1, scaleZ = cardList[1].data.Transform.scaleZ } 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 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() 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 -- 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) return __bundle_require("__root")