c33d0386b7
Because of the mix of global and relative positioning this ended up being very complex. Tried to make sure the comments were thorough, but open to any improvement suggestions.
236 lines
8.9 KiB
Plaintext
236 lines
8.9 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: 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, 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: 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
|