545 lines
18 KiB
Plaintext
545 lines
18 KiB
Plaintext
-- 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") |