248 lines
9.5 KiB
Plaintext
248 lines
9.5 KiB
Plaintext
-- 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
|
|
-- set proper scale for minicards
|
|
card.data.Transform.scaleX = 0.6
|
|
card.data.Transform.scaleZ = 0.6
|
|
table.insert(miniCards, card)
|
|
else
|
|
table.insert(standardCards, card)
|
|
end
|
|
end
|
|
|
|
-- Spawn each of the three types individually. Y position 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
|
|
return spawnObjectData({
|
|
data = cardList[1].data,
|
|
position = pos,
|
|
rotation = rot,
|
|
callback_function = callback
|
|
})
|
|
end
|
|
|
|
-- For multiple cards, construct a deck and spawn that
|
|
local deckScaleX = cardList[1].data.Transform.scaleX
|
|
local deckScaleZ = cardList[1].data.Transform.scaleZ
|
|
local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ)
|
|
|
|
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
|
|
|
|
return 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(deckScaleX, deckScaleZ)
|
|
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
|
|
-- 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 = deckScaleX or 1,
|
|
scaleY = 1,
|
|
scaleZ = deckScaleZ or 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
|