-- 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("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/AllCardsBag") end) __bundle_register("playercards/AllCardsBag", function(require, _LOADED, __bundle_register, __bundle_modules) local guidReferenceApi = require("core/GUIDReferenceApi") local cardIdIndex = {} local classAndLevelIndex = {} local basicWeaknessList = {} local uniqueWeaknessList = {} local cycleIndex = {} local indexingDone = false local otherCardsDetected = false function onLoad() self.addContextMenuItem("Rebuild Index", startIndexBuild) math.randomseed(os.time()) Wait.frames(startIndexBuild, 30) 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. function rebuildIndexForHotfix() if indexingDone then startIndexBuild() end end -- Resets all current bag indexes function clearIndexes() indexingDone = false cardIdIndex = {} classAndLevelIndex = {} classAndLevelIndex["Guardian-upgrade"] = {} classAndLevelIndex["Seeker-upgrade"] = {} classAndLevelIndex["Mystic-upgrade"] = {} classAndLevelIndex["Survivor-upgrade"] = {} classAndLevelIndex["Rogue-upgrade"] = {} classAndLevelIndex["Neutral-upgrade"] = {} classAndLevelIndex["Guardian-level0"] = {} classAndLevelIndex["Seeker-level0"] = {} classAndLevelIndex["Mystic-level0"] = {} classAndLevelIndex["Survivor-level0"] = {} classAndLevelIndex["Rogue-level0"] = {} classAndLevelIndex["Neutral-level0"] = {} cycleIndex = {} basicWeaknessList = {} uniqueWeaknessList = {} end -- Clears the bag indexes and starts the coroutine to rebuild the indexes function startIndexBuild() clearIndexes() startLuaCoroutine(self, "buildIndex") end function onObjectLeaveContainer(container, _) if container == self then broadcastToAll("Removing cards from the All Player Cards bag may break some functions.", "Red") end end -- Create the card indexes by iterating all cards in the bag, parsing their metadata -- and creating the keyed lookup tables for the cards. This is a coroutine which will -- spread the workload by processing 20 cards before yielding. function buildIndex() local cardCount = 0 indexingDone = false otherCardsDetected = false -- process the allcardsbag itself for _, cardData in ipairs(self.getData().ContainedObjects) do addCardToIndex(cardData) cardCount = cardCount + 1 if cardCount > 19 then cardCount = 0 coroutine.yield(0) end end -- process hotfix bags (and the additional playercards bag) for _, hotfixBag in ipairs(getObjectsWithTag("AllCardsHotfix")) do local hotfixData = hotfixBag.getData() -- if the bag is empty, continue with the next bag if not hotfixData.ContainedObjects then goto nextBag end for _, cardData in ipairs(hotfixData.ContainedObjects) do if cardData.ContainedObjects then -- process containers for _, deepCardData in ipairs(cardData.ContainedObjects) do addCardToIndex(deepCardData) cardCount = cardCount + 1 if cardCount > 19 then cardCount = 0 coroutine.yield(0) end end else -- process single cards addCardToIndex(cardData) cardCount = cardCount + 1 if cardCount > 19 then cardCount = 0 coroutine.yield(0) end end end ::nextBag:: end buildSupplementalIndexes() updatePlayerCardPanel() indexingDone = true return 1 end -- Adds a card to any indexes it should be a part of, based on its metadata ---@param cardData table TTS object data for the card function addCardToIndex(cardData) -- using the more efficient 'json.parse()' to speed this process up local status, cardMetadata = pcall(function() return json.parse(cardData.GMNotes) end) -- if an error happens, fallback to the regular parser if status ~= true or cardMetadata == nil then log("Fast parser failed for " .. cardData.Nickname .. ", using old parser instead.") cardMetadata = JSON.decode(cardData.GMNotes) end -- if metadata was not valid JSON or empty, don't add the card if not cardMetadata == nil then log("Error parsing " .. cardData.Nickname) return end -- use the ZoopGuid as fallback if no id present cardMetadata.id = cardMetadata.id or cardMetadata.TtsZoopGuid cardIdIndex[cardMetadata.id] = { data = cardData, metadata = cardMetadata } -- also add data for alternate ids if cardMetadata.alternate_ids ~= nil then for _, alternateId in ipairs(cardMetadata.alternate_ids) do cardIdIndex[alternateId] = { data = cardData, metadata = cardMetadata } end end end -- Creates the supplemental indexes for classes, weaknesses etc. function buildSupplementalIndexes() for cardId, card in pairs(cardIdIndex) do -- If the ID key and the metadata ID don't match this is a duplicate card created by an alternate_id, and we should skip it if cardId == card.metadata.id then -- Add card to the basic weakness list, if appropriate. Some weaknesses have multiple copies, and are added multiple times if card.metadata.weakness then table.insert(uniqueWeaknessList, card.metadata.id) if card.metadata.basicWeaknessCount ~= nil then for i = 1, card.metadata.basicWeaknessCount do table.insert(basicWeaknessList, card.metadata.id) end end end -- Excludes signature cards (which have no class or level) if card.metadata.class ~= nil and card.metadata.level ~= nil then local upgradeKey = "-level0" if card.metadata.level > 0 then upgradeKey = "-upgrade" end -- parse classes (separated by "|") and add the card to the appropriate class and level indices for str in card.metadata.class:gmatch("([^|]+)") do table.insert(classAndLevelIndex[str .. upgradeKey], card.metadata.id) end -- add to cycle index local cycleName = card.metadata.cycle if cycleName ~= nil then cycleName = string.lower(cycleName) -- remove "return to " from cycle names cycleName = cycleName:gsub("return to ", "") -- override cycle name for night of the zealot cycleName = cycleName:gsub("the night of the zealot", "core") else -- track cards without defined cycle (should only be fan-made cards) cycleName = "other" otherCardsDetected = true end -- maybe initialize table if cycleIndex[cycleName] == nil then cycleIndex[cycleName] = {} end table.insert(cycleIndex[cycleName], card.metadata.id) end end end -- sort class and level indices for _, indexTable in pairs(classAndLevelIndex) do table.sort(indexTable, cardComparator) end -- sort cycle indices for _, indexTable in pairs(cycleIndex) do table.sort(indexTable) end -- sort weakness indices table.sort(basicWeaknessList, cardComparator) table.sort(uniqueWeaknessList, cardComparator) end -- Comparison function used to sort the class card bag indexes. Sorts by card level, then name, then subname. function cardComparator(id1, id2) local card1 = cardIdIndex[id1] local card2 = cardIdIndex[id2] if card1.metadata.level ~= card2.metadata.level then return card1.metadata.level < card2.metadata.level elseif card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname < card2.data.Nickname else return card1.data.Description < card2.data.Description end end -- inform the player card panel about the presence of other cards (no cycle -> fan-made) function updatePlayerCardPanel() local panel = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayerCardPanel") panel.call("createXML", otherCardsDetected) end ---@return boolean: If true, the bag is currently not indexing and ready to be accessed function isIndexReady() if not indexingDone then broadcastToAll("Still loading player cards, please try again in a few seconds", { 0.9, 0.2, 0.2 }) end return indexingDone end -- Returns a specific card from the bag, based on ArkhamDB ID ---@param params table ID of the card to retrieve ---@return table: If the indexes are still being constructed, returns an empty table. -- Otherwise, a single table with the following fields -- data: TTS object data, suitable for spawning the card -- metadata: Table of parsed metadata function getCardById(params) if not isIndexReady() then return {} end return cardIdIndex[params.id] end -- Returns a list of cards from the bag matching a class and level (0 or upgraded) ---@param params table -- class: String class to retrieve ("Guardian", "Seeker", etc) -- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0 ---@return table: If the indexes are still being constructed, returns an empty table. -- Otherwise, a list of tables, each with the following fields -- data: TTS object data, suitable for spawning the card -- metadata: Table of parsed metadata function getCardsByClassAndLevel(params) if not isIndexReady() then return {} end local upgradeKey = "-level0" if params.upgraded then upgradeKey = "-upgrade" end return classAndLevelIndex[params.class .. upgradeKey] end -- Returns a list of cards from the bag matching a cycle ---@param params table -- cycle: String cycle to retrieve ("The Scarlet Keys" etc.) -- sortByMetadata: true to sort the table by metadata instead of ID ---@return table: If the indexes are still being constructed, returns an empty table. -- Otherwise, a list of tables, each with the following fields -- data: TTS object data, suitable for spawning the card -- metadata: Table of parsed metadata function getCardsByCycle(params) if not isIndexReady() then return {} end if not params.sortByMetadata then return cycleIndex[string.lower(params.cycle)] end -- sort list by metadata (useful for custom cards without proper IDs) local cardList = {} for _, id in ipairs(cycleIndex[string.lower(params.cycle)]) do table.insert(cardList, id) end table.sort(cardList, metadataSortFunction) return cardList end -- sorts cards by metadata: class, type, level, name and then description function metadataSortFunction(id1, id2) local card1 = cardIdIndex[id1] local card2 = cardIdIndex[id2] -- extract class per card local classValue1 = getClassValueFromString(card1.metadata.class) local classValue2 = getClassValueFromString(card2.metadata.class) -- conversion tables to simplify type sorting local typeConversion = { Asset = 1, Event = 2, Skill = 3 } if classValue1 ~= classValue2 then return classValue1 < classValue2 elseif typeConversion[card1.metadata.type] ~= typeConversion[card2.metadata.type] then return typeConversion[card1.metadata.type] < typeConversion[card2.metadata.type] elseif card1.metadata.level ~= card2.metadata.level then return card1.metadata.level < card2.metadata.level elseif card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname < card2.data.Nickname else return card1.data.Description < card2.data.Description end end -- helper function to calculate the class value for sorting from the "|" separated string function getClassValueFromString(s) local classValueList = { Guardian = 1, Seeker = 2, Rogue = 3, Mystic = 4, Survivor = 5, Neutral = 6 } local classValue = 0 for str in s:gmatch("([^|]+)") do -- this sorts multiclass cards classValue = classValue * 10 + classValueList[str] end return classValue 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. -- Parameter array must contain these fields to define the search: -- name: String or string fragment to search for names -- exact: Whether the name match should be exact function getCardsByName(params) local name = params.name local exact = params.exact local results = {} -- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs local addedCards = {} for _, cardData in pairs(cardIdIndex) do if (not addedCards[cardData.metadata.id]) then if (exact and (string.lower(cardData.data.Nickname) == string.lower(name))) or (not exact and string.find(string.lower(cardData.data.Nickname), string.lower(name), 1, true)) then table.insert(results, cardData) addedCards[cardData.metadata.id] = true end end end return results 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 function getRandomWeaknessId() local availableWeaknesses = buildAvailableWeaknesses() if #availableWeaknesses > 0 then return availableWeaknesses[math.random(#availableWeaknesses)] end end -- Constructs a list of available basic weaknesses by starting with the full pool of basic -- weaknesses then removing any which are currently in the play or deck construction areas ---@param traits? string Trait(s) to use as filter ---@return table: Array of weakness IDs which are valid to choose from function buildAvailableWeaknesses(traits) local weaknessesInPlay = {} local allObjects = getAllObjects() for _, object in ipairs(allObjects) do if object.type == "Deck" then for _, cardData in ipairs(object.getData().ContainedObjects) do incrementWeaknessCount(weaknessesInPlay, JSON.decode(cardData.GMNotes)) end elseif object.type == "Card" then incrementWeaknessCount(weaknessesInPlay, JSON.decode(object.getGMNotes())) end end local availableWeaknesses = {} for _, weaknessId in ipairs(basicWeaknessList) do if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] > 0) then weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1 else if traits then -- split the string into separate traits (separated by "|") local allowedTraits = {} for str in traits:gmatch("([^|]+)") do -- remove dots str = str:gsub("[%.]", "") -- remove leading and trailing whitespace str = str:match("^%s*(.-)%s*$") -- make sure string ends with a dot str = string.lower(str .. ".") table.insert(allowedTraits, str) end -- make sure the trait is present on the weakness local card = cardIdIndex[weaknessId] for _, allowedTrait in ipairs(allowedTraits) do if string.contains(string.lower(card.metadata.traits), allowedTrait) then table.insert(availableWeaknesses, weaknessId) break end end else table.insert(availableWeaknesses, weaknessId) end end end return availableWeaknesses end function getBasicWeaknesses() return basicWeaknessList end function getUniqueWeaknesses() return uniqueWeaknessList end -- Helper function that adds one to the table entry for the number of weaknesses in play function incrementWeaknessCount(table, cardMetadata) if isBasicWeakness(cardMetadata) then if table[cardMetadata.id] == nil then table[cardMetadata.id] = 1 else table[cardMetadata.id] = table[cardMetadata.id] + 1 end end end function isBasicWeakness(cardMetadata) return cardMetadata ~= nil and cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 end end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local GUIDReferenceApi = {} local function getGuidHandler() return getObjectFromGUID("123456") end -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object GUIDReferenceApi.editIndex = function(owner, type, guid) return getGuidHandler().call("editIndex", { owner = owner, type = type, guid = guid }) end -- Returns the owner of an object or the object it's located on ---@param object tts__GameObject Object for this search ---@return string: Parent of the object or object it's located on GUIDReferenceApi.getOwnerOfObject = function(object) return getGuidHandler().call("getOwnerOfObject", object) end return GUIDReferenceApi end end) return __bundle_require("__root")