SCED/src/playercards/PlayerCardPanel.ttslua

626 lines
21 KiB
Plaintext
Raw Normal View History

require("playercards/PlayerCardPanelData")
local spawnBag = require("playercards/spawnbag/SpawnBag")
local arkhamDb = require("arkhamdb/ArkhamDb")
-- Size and position information for the three rows of class buttons
local CIRCLE_BUTTON_SIZE = 250
local CLASS_BUTTONS_X_OFFSET = 0.1325
local INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)
local LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)
local UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)
-- Size and position information for the two blocks of other buttons
local MISC_BUTTONS_X_OFFSET = 0.155
local WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)
local OTHER_ROW_START = Vector(0.605, 0.1, 0.666)
-- Size and position information for the Cycle (box) buttons
local CYCLE_BUTTON_SIZE = 468
local CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)
local CYCLE_COLUMN_COUNT = 3
local CYCLE_BUTTONS_X_OFFSET = 0.267
local CYCLE_BUTTONS_Z_OFFSET = 0.2665
local ALL_CARDS_BAG_GUID = "15bb07"
local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }
local TRANSPARENT = { 0, 0, 0, 0 }
local STARTER_DECK_MODE_STARTERS = "starters"
local STARTER_DECK_MODE_CARDS_ONLY = "cards"
local FACE_UP_ROTATION = { x = 0, y = 270, z = 0}
local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180}
-- Coordinates to begin laying out cards. These vary based on the cards that are being placed
local START_POSITIONS = {
classCards = Vector(58.384, 1.36, 92.4),
investigator = Vector(60, 1.36, 86),
cycle = Vector(48, 1.36, 92.4),
other = Vector(56, 1.36, 86),
summonedServitor = Vector(55.5, 1.36, 60.2),
randomWeakness = Vector(55, 1.36, 75)
}
-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out
local CARD_ROW_OFFSET = 3.7
local CARD_GROUP_OFFSET = 2
-- Position offsets for investigator decks in investigator mode, defines the spacing for how the
-- rows and columns are laid out
local INVESTIGATOR_POSITION_SHIFT_ROW = Vector(-11, 0, 0)
local INVESTIGATOR_POSITION_SHIFT_COL = Vector(0, 0, -6)
local INVESTIGATOR_MAX_COLS = 6
-- Positions relative to the minicard to place other stacks. Both signature card piles and starter
-- decks use SIGNATURE_OFFSET
local INVESTIGATOR_CARD_OFFSET = Vector(-2.55, 0, 0)
local INVESTIGATOR_SIGNATURE_OFFSET = Vector(-5.75, 0, 0)
local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" }
local CYCLE_LIST = {
"Core",
"The Dunwich Legacy",
"The Path to Carcosa",
"The Forgotten Age",
"The Circle Undone",
"The Dream-Eaters",
"The Innsmouth Conspiracy",
"Edge of the Earth",
"The Scarlet Keys",
"Investigator Packs"
}
local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
function onSave()
local saveState = {
spawnBagState = spawnBag.getStateForSave(),
}
return JSON.encode(saveState)
end
function onLoad(savedData)
arkhamDb.initialize()
if (savedData ~= nil) then
local saveState = JSON.decode(savedData) or { }
if (saveState.spawnBagState ~= nil) then
spawnBag.loadFromSave(saveState.spawnBagState)
end
end
createButtons()
end
function createButtons()
createInvestigatorButtons()
createLevelZeroButtons()
createUpgradedButtons()
createWeaknessButtons()
createOtherButtons()
createCycleButtons()
createClearButton()
-- Create investigator mode buttons last so the indexes are set when we need to update them
createInvestigatorModeButtons()
end
function createInvestigatorButtons()
local invButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = INVESTIGATOR_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
invButtonParams.click_function = "spawnInvestigators" .. class
invButtonParams.position = buttonPos
self.createButton(invButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)
end
end
function createLevelZeroButtons()
local l0ButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = LEVEL_ZERO_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
l0ButtonParams.click_function = "spawnBasic" .. class
l0ButtonParams.position = buttonPos
self.createButton(l0ButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)
end
end
function createUpgradedButtons()
local upgradedButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = UPGRADED_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
upgradedButtonParams.click_function = "spawnUpgraded" .. class
upgradedButtonParams.position = buttonPos
self.createButton(upgradedButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)
end
end
function createWeaknessButtons()
local weaknessButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = WEAKNESS_ROW_START:copy()
weaknessButtonParams.click_function = "spawnWeaknesses"
weaknessButtonParams.tooltip = "Basic Weaknesses"
weaknessButtonParams.position = buttonPos
self.createButton(weaknessButtonParams)
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
weaknessButtonParams.click_function = "spawnRandomWeakness"
weaknessButtonParams.tooltip = "Random Weakness"
weaknessButtonParams.position = buttonPos
self.createButton(weaknessButtonParams)
end
function createOtherButtons()
local otherButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = OTHER_ROW_START:copy()
otherButtonParams.click_function = "spawnBonded"
otherButtonParams.tooltip = "Bonded Cards"
otherButtonParams.position = buttonPos
self.createButton(otherButtonParams)
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
otherButtonParams.click_function = "spawnUpgradeSheets"
otherButtonParams.tooltip = "Customization Upgrade Sheets"
otherButtonParams.position = buttonPos
self.createButton(otherButtonParams)
end
function createCycleButtons()
local cycleButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CYCLE_BUTTON_SIZE,
width = CYCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = CYCLE_BUTTON_START:copy()
local rowCount = 0
local colCount = 0
for _, cycle in ipairs(CYCLE_LIST) do
cycleButtonParams.click_function = "spawnCycle" .. cycle
cycleButtonParams.position = buttonPos
cycleButtonParams.tooltip = cycle
self.createButton(cycleButtonParams)
self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)
colCount = colCount + 1
-- If we've reached the end of a row, shift down and back to the first column
if colCount >= CYCLE_COLUMN_COUNT then
buttonPos = CYCLE_BUTTON_START:copy()
rowCount = rowCount + 1
colCount = 0
buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount
if rowCount == 3 then
-- Account for centered button on the final row
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
end
else
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
end
end
end
function createClearButton()
self.createButton({
function_owner = self,
click_function = "deleteAll",
position = Vector(0, 0.1, 0.852),
rotation = Vector(0, 0, 0),
height = 170,
width = 750,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
})
end
function createInvestigatorModeButtons()
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
self.createButton({
function_owner = self,
click_function = "setCardsOnlyMode",
position = Vector(0.251, 0.1, -0.322),
rotation = Vector(0, 0, 0),
height = 170,
width = 760,
scale = Vector(0.25, 1, 0.25),
color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR
})
self.createButton({
function_owner = self,
click_function = "setStarterDeckMode",
position = Vector(0.66, 0.1, -0.322),
rotation = Vector(0, 0, 0),
height = 170,
width = 760,
scale = Vector(0.25, 1, 0.25),
color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT
})
local checkX = starterMode and 0.52 or 0.11
self.createButton({
function_owner = self,
label = "✓",
click_function = "doNothing",
position = Vector(checkX, 0.11, -0.317),
rotation = Vector(0, 0, 0),
height = 0,
width = 0,
scale = Vector(0.3, 1, 0.3),
font_color = { 0, 0, 0 },
color = { 1, 1, 1 }
})
end
function setStarterDeckMode()
starterDeckMode = STARTER_DECK_MODE_STARTERS
updateStarterModeButtons()
end
function setCardsOnlyMode()
starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
updateStarterModeButtons()
end
function updateStarterModeButtons()
local buttonCount = #self.getButtons()
-- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size
self.removeButton(buttonCount - 1)
self.removeButton(buttonCount - 2)
self.removeButton(buttonCount - 3)
createInvestigatorModeButtons()
end
-- Deletes all cards currently placed on the table
function deleteAll()
spawnBag.recall(true)
end
-- Spawn an investigator group, based on the current UI setting for either investigators or starter
-- decks.
---@param groupName String. Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnInvestigatorGroup(groupName)
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
spawnBag.recall(true)
Wait.frames(function()
if starterMode then
spawnStarters(groupName)
else
spawnInvestigators(groupName)
end
end, 2)
end
-- Spawn cards for all investigators in the given group. This creates piles for all defined
-- investigator cards and minicards as well as the signature cards.
---@param groupName String. Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnInvestigators(groupName)
local position = Vector(START_POSITIONS.investigator)
local col = 1
local row = 1
if INVESTIGATOR_GROUPS[groupName] == nil then
printToAll("No " .. groupName .. " data yet")
return
end
for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do
for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(
investigatorName, INVESTIGATORS[investigatorName], position, false)) do
spawnBag.spawn(spawnSpec)
end
position:add(INVESTIGATOR_POSITION_SHIFT_COL)
col = col + 1
if col > INVESTIGATOR_MAX_COLS then
col = 1
position = Vector(START_POSITIONS.investigator)
position:add(Vector(
INVESTIGATOR_POSITION_SHIFT_ROW.x * row,
INVESTIGATOR_POSITION_SHIFT_ROW.z * row,
INVESTIGATOR_POSITION_SHIFT_ROW.z * row))
row = row + 1
end
end
end
-- Creates the spawn spec for the investigator's signature cards.
---@param investigatorName String. Name of the investigator, matching a key in
--- InvestigatorPanelData
---@param investigatorData Table. Spawn definition for the investigator, retrieved from
--- INVESTIGATORS
---@param position Vector. Where to spawn the minicard; investigagor cards will be placed below
function buildInvestigatorSpawnSpec(investigatorName, investigatorData, position)
local sigPos = Vector(position):add(INVESTIGATOR_SIGNATURE_OFFSET)
local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position)
table.insert(spawns, {
name = investigatorName.."signatures",
cards = investigatorData.signatures,
globalPos = sigPos,
rotation = FACE_UP_ROTATION,
})
return spawns
end
-- Builds the spawn specs for minicards and investigator cards. These are common enough to be
-- shared, and will only differ in whether they spawn the full stack of possible investigator and
-- minicards, or only the first of each.
---@param investigatorName String. Name of the investigator, matching a key in
--- InvestigatorPanelData
---@param investigatorData Table. Spawn definition for the investigator, retrieved from
--- INVESTIGATORS
---@param position Vector. Where to spawn the minicard; investigagor cards will be placed below
---@param oneCardOnly Boolean. If true, will spawn only the first card in the investigator card
--- and minicard lists. Otherwise, spawn them all in a deck
function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly)
local cardPos = Vector(position):add(INVESTIGATOR_CARD_OFFSET)
return {
{
name = investigatorName.."minicards",
cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,
globalPos = position,
rotation = FACE_UP_ROTATION,
},
{
name = investigatorName.."cards",
cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,
globalPos = cardPos,
rotation = FACE_UP_ROTATION,
},
}
end
-- Spawns all starter decks (single minicard and investigator card, plus the starter deck) for
-- investigators in the given group.
---@param groupName String. Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnStarters(groupName)
local position = Vector(START_POSITIONS.investigator)
local col = 1
local row = 1
for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do
spawnStarterDeck(investigatorName, INVESTIGATORS[investigatorName], position)
position:add(INVESTIGATOR_POSITION_SHIFT_COL)
col = col + 1
if col > INVESTIGATOR_MAX_COLS then
col = 1
position = Vector(START_POSITIONS.investigator)
position:add(Vector(
INVESTIGATOR_POSITION_SHIFT_ROW.x * row,
INVESTIGATOR_POSITION_SHIFT_ROW.z * row,
INVESTIGATOR_POSITION_SHIFT_ROW.z * row))
row = row + 1
end
end
end
-- Spawns the defined starter deck for the given investigator's.
---@param investigatorName String. Name of the investigator, matching a key in
--- InvestigatorPanelData
function spawnStarterDeck(investigatorName, investigatorData, position)
for _, spawnSpec in ipairs(
buildCommonSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position, true)) do
spawnBag.spawn(spawnSpec)
end
local deckPos = Vector(position):add(INVESTIGATOR_SIGNATURE_OFFSET)
arkhamDb.getDecklist("None", investigatorData.starterDeck, true, false, false, function(slots)
local cardIdList = { }
for id, count in pairs(slots) do
for i = 1, count do
table.insert(cardIdList, id)
end
end
spawnBag.spawn({
name = investigatorName.."starter",
cards = cardIdList,
globalPos = deckPos,
rotation = FACE_DOWN_ROTATION
})
end)
end
-- Clears the currently placed cards, then places cards for the given class and level spread
---@param cardClass String. Class to place ("Guardian", "Seeker", etc)
---@param isUpgraded Boolean. If true, spawn the Level 1-5 cards. Otherwise, Level 0.
function spawnClassCards(cardClass, isUpgraded)
spawnBag.recall(true)
Wait.frames(function() placeClassCards(cardClass, isUpgraded) end, 2)
end
-- Spawn the class cards.
---@param cardClass String. Class to place ("Guardian", "Seeker", etc)
---@param isUpgraded Boolean. If true, spawn the Level 1-5 cards. Otherwise, Level 0.
function placeClassCards(cardClass, isUpgraded)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local indexReady = allCardsBag.call("isIndexReady")
if (not indexReady) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return
end
local cardIdList = allCardsBag.call("getCardsByClassAndLevel", {class = cardClass, upgraded = isUpgraded})
local skillList = { }
local eventList = { }
local assetList = { }
for _, cardId in ipairs(cardIdList) do
local cardMetadata = allCardsBag.call("getCardById", { id = cardId }).metadata
if (cardMetadata.type == "Skill") then
table.insert(skillList, cardId)
elseif (cardMetadata.type == "Event") then
table.insert(eventList, cardId)
elseif (cardMetadata.type == "Asset") then
table.insert(assetList, cardId)
end
end
local groupPos = Vector(START_POSITIONS.classCards)
if #skillList > 0 then
spawnBag.spawn({
name = cardClass .. (isUpgraded and "upgraded" or "basic"),
cards = skillList,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#skillList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #eventList > 0 then
spawnBag.spawn({
name = cardClass .. "event" .. (isUpgraded and "upgraded" or "basic"),
cards = eventList,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#eventList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #assetList > 0 then
spawnBag.spawn({
name = cardClass .. "asset" .. (isUpgraded and "upgraded" or "basic"),
cards = assetList,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
end
-- Spawns the investigator sets and all cards for the given cycle
---@param cycle String Name of a cycle, should match the standard used in card metadata
function spawnCycle(cycle)
spawnBag.recall(true)
spawnInvestigators(cycle)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local indexReady = allCardsBag.call("isIndexReady")
if (not indexReady) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return
end
local cycleCardList = allCardsBag.call("getCardsByCycle", cycle)
local copiedList = { }
for i, id in ipairs(cycleCardList) do
copiedList[i] = id
end
spawnBag.spawn({
name = "cycle"..cycle,
cards = copiedList,
globalPos = START_POSITIONS.cycle,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnBonded()
spawnBag.recall(true)
spawnBag.spawn({
name = "bonded",
cards = BONDED_CARD_LIST,
globalPos = START_POSITIONS.classCards,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnUpgradeSheets()
spawnBag.recall(true)
spawnBag.spawn({
name = "upgradeSheets",
cards = UPGRADE_SHEET_LIST,
globalPos = START_POSITIONS.classCards,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
spawnBag.spawn({
name = "servitor",
cards = { "09080-m" },
globalPos = START_POSITIONS.summonedServitor,
rotation = FACE_UP_ROTATION,
})
end
-- Clears the current cards, and places all basic weaknesses on the table.
function spawnWeaknesses()
spawnBag.recall(true)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local indexReady = allCardsBag.call("isIndexReady")
if (not indexReady) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return
end
local weaknessIdList = allCardsBag.call("getUniqueWeaknesses")
local copiedList = { }
for i, id in ipairs(weaknessIdList) do
copiedList[i] = id
end
local groupPos = Vector(START_POSITIONS.classCards)
spawnBag.spawn({
name = "weaknesses",
cards = copiedList,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#copiedList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
spawnBag.spawn({
name = "evolvedWeaknesses",
cards = EVOLVED_WEAKNESSES,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnRandomWeakness()
spawnBag.recall(true)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local weaknessId = allCardsBag.call("getRandomWeaknessId")
if (weaknessId == nil) then
broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2})
return
end
spawnBag.spawn({
name = "randomWeakness",
cards = { weaknessId },
globalPos = START_POSITIONS.randomWeakness,
rotation = FACE_UP_ROTATION,
})
end