-- 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("playercards/spawnbag/BondedBag", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/spawnbag/SpawnBag") SPAWN_SPEC = { name = "BondedCards", cards = { "05314", -- Soothing Melody "06277", -- Wish Eater "06019", -- Bloodlust "06022", -- Pendant of the Queen "05317", -- Blood-rite "06113", -- Essence of the Dream "06028", -- Stars Are Right "06025", -- Guardian of the Crystallizer "06283", -- Unbound Beast "06032", -- Zeal "06031", -- Hope "06033", -- Augur "06331", -- Dream Parasite "06015a", -- Dream-Gate }, globalPos = { x = -33.88, y = 1.5, z = 85.61 }, rotation = { x = 0, y = 270, z = 0 }, spread = true, } function onLoad(savedData) if (savedData ~= nil) then local saveState = JSON.decode(savedData) if (saveState.spawnBagState ~= nil) then SpawnBag.loadFromSave(saveState.spawnBagState) end end createActionButtons() end function onSave() local saveState = { spawnBagState = SpawnBag.getStateForSave(), } return JSON.encode(saveState) end function createActionButtons() self.createButton({ label="Place", click_function="buttonClick_place", function_owner=self, position={1,0.1,2.1}, rotation={0,0,0}, height=350, width=800, font_size=250, color={0,0,0}, font_color={1,1,1} }) self.createButton({ label="Recall", click_function="buttonClick_recall", function_owner=self, position={-1,0.1,2.1}, rotation={0,0,0}, height=350, width=800, font_size=250, color={0,0,0}, font_color={1,1,1} }) end function buttonClick_place() SpawnBag.spawn(SPAWN_SPEC) end -- Recalls objects to bag from table function buttonClick_recall() SpawnBag.recall() end end) __bundle_register("playercards/spawnbag/SpawnBag", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/PlayerCardSpawner") -- Allows spawning of defined lists of cards which will be created from the template in the All -- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while -- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the -- spawned objects. Objects moved out of this area will not be cleaned up. -- -- SpawnSpec: Spawning requires a spawn specification with the following structure: -- { -- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned, -- but each requires a separate name -- cards: A list of card IDs to be spawned -- globalPos: Where the spawned objects should be placed, in global coordinates. This should be -- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 } -- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with -- globalPos, this should be a valid Vector with x, y, and z defined -- spread: Optional. If present and true, cards will be spawned next to each other in a spread -- moving to the right. globalPos will define the location of the first card, each after that -- will be moved a predefined distance -- } -- See BondedBag.ttslua for an example SpawnBag = { } -- To assist debugging, will draw a box around the recall zone when it's set up local SHOW_RECALL_ZONE = false local ALL_CARDS_GUID = "15bb07" -- Distance to expand the recall zone around any added object. local RECALL_BUFFER_X = 0.9 local RECALL_BUFFER_Z = 0.5 -- In order to mimic the behavior of the previous memory buttons we use a temporary bag when -- recalling objects. This bag is tiny and transparent, and will be placed at the same location as -- this object. Once all placed cards are recalled bag to this bag, it will be destroyed local RECALL_BAG = { Name = "Bag", Transform = { scaleX = 0.01, scaleY = 0.01, scaleZ = 0.01, }, ColorDiffuse = { r = 0, g = 0, b = 0, a = 0, }, Locked = true, Grid = true, Snap = false, Tooltip = false, } -- Tracks what has been placed by this "bag" so they can be recalled local placedSpecs = { } local placedObjectGuids = { } local recallZone = nil -- Loads a table of saved state, extracted during the parent object's onLoad SpawnBag.loadFromSave = function(saveTable) placedSpecs = saveTable.placed placedObjectGuids = saveTable.placedObjects recallZone = saveTable.recall end -- Generates a table of save state that can be included in the parent object's onSave SpawnBag.getStateForSave = function() return { placed = placedSpecs, placedObjects = placedObjectGuids, recall = recallZone, } end -- Places the given spawnSpec on the table. See SpawnBag.ttslua header for spawnSpec table data and -- examples SpawnBag.spawn = function(spawnSpec) -- Limit to one placement at a time if (placedSpecs[spawnSpec.name]) then return end if (spawnSpec == nil) then -- TODO: error here return end local cardsToSpawn = { } local allCardsBag = getObjectFromGUID(ALL_CARDS_GUID) for _, cardId in ipairs(spawnSpec.cards) do local cardData = allCardsBag.call("getCardById", { id = cardId }) if (cardData ~= nil) then table.insert(cardsToSpawn, cardData) else -- TODO: error here end end if (spawnSpec.spread) then Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, recordPlacedObject) else Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, recordPlacedObject) end placedSpecs[spawnSpec.name] = true end -- Recalls all spawned objects to the bag, and clears the placedObjectGuids list SpawnBag.recall = function() local trash = spawnObjectData({data = RECALL_BAG, position = self.getPosition()}) for guid, _ in pairs(placedObjectGuids) do local obj = getObjectFromGUID(guid) if (obj ~= nil) then if (isInRecallZone(obj)) then trash.putObject(obj) end placedObjectGuids[guid] = nil end end trash.destruct() -- We've recalled everything we can, some cards may have been moved out of the -- card area. Just reset at this point. placedSpecs = { } placedObjectGuids = { } recallZone = nil end -- Callback for when an object has been spawned. Tracks the object for later recall and updates the -- recall zone. function recordPlacedObject(spawned) placedObjectGuids[spawned.getGUID()] = true expandRecallZone(spawned) end -- Expands the current recall zone based on the position of the given object. The recall zone will -- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer function expandRecallZone(spawnedCard) local pos = spawnedCard.getPosition() if (recallZone == nil) then -- First card out of the bag, initialize surrounding that recallZone = { } recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z } recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z } return else if (pos.x > recallZone.upperLeft.x) then recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X end if (pos.x < recallZone.lowerRight.x) then recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X end if (pos.z > recallZone.upperLeft.z) then recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z end if (pos.z < recallZone.lowerRight.z) then recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z end end if (SHOW_RECALL_ZONE) then local y = 1.5 local thick = 0.05 Global.setVectorLines({ { points = { {recallZone.upperLeft.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.lowerRight.z} }, color = {1,0,0}, thickness = thick, rotation = {0,0,0}, }, { points = { {recallZone.upperLeft.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.lowerRight.z} }, color = {1,0,0}, thickness = thick, rotation = {0,0,0}, }, { points = { {recallZone.lowerRight.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.upperLeft.z} }, color = {1,0,0}, thickness = thick, rotation = {0,0,0}, }, { points = { {recallZone.lowerRight.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.upperLeft.z} }, color = {1,0,0}, thickness = thick, rotation = {0,0,0}, }, }) end end -- Checks to see if the given object is in the current recall zone. If there isn't a recall zone, -- will return true so that everything can be easily cleaned up. function isInRecallZone(obj) if (recallZone == nil) then return true end local pos = obj.getPosition() return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z) 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 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: A list of Player Card data structures (data/metadata) -- @param pos Position table where the cards should be spawned (global) -- @param rot Rotation 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.x, rot.y - 90, rot.z}, 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, rot, sort, callback) if (sort) then table.sort(cardList, Spawner.cardComparator) end local position = { x = startPos.x, y = startPos.y, z = startPos.z } for _, card in ipairs(cardList) do Spawner.spawn({ card }, position, rot, callback) position.z = position.z + SPREAD_Z_SHIFT 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: A list of Player Card data structures (data/metadata) -- @param pos Position table where the cards should be spawned (global) -- @param rot Rotation table 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 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, } for _, spawnCard in ipairs(cardList) do Spawner.addCardToDeck(deck, spawnCard.data) 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: TTS deck data structure to add to ---@param card: 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 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 First 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: 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are -- irrelevant as they provide only grouping and the order between them doesn't matter. Spawner.getpbcn = function(metadata) if metadata.permanent then return 1 elseif metadata.bonded_to ~= nil then return 2 else -- Normal card return 3 end end -- Comparison function used to sort the cards in a deck. Groups bonded or -- permanent cards first, then sorts within theose types by name/subname. -- Normal cards will sort in standard alphabetical order, while -- permanent/bonded/customizable will be in reverse alphabetical order. -- -- Since cards spawn in the order provided by this comparator, with the first -- cards ending up at the bottom of a pile, this ordering will spawn in reverse -- alphabetical order. This presents the cards in order for non-face-down -- areas, and presents them in order when Searching the face-down deck. Spawner.cardComparator = function(card1, card2) local pbcn1 = Spawner.getpbcn(card1.metadata) local pbcn2 = Spawner.getpbcn(card2.metadata) if pbcn1 ~= pbcn2 then return pbcn1 > pbcn2 end if pbcn1 == 3 then if card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname < card2.data.Nickname end return card1.data.Description < card2.data.Description else if card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname > card2.data.Nickname end return card1.data.Description > card2.data.Description end end end) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/spawnbag/BondedBag") end) return __bundle_require("__root")