require("playercards/PlayerCardPanelData") local allCardsBagApi = require("playercards/AllCardsBagApi") local arkhamDb = require("arkhamdb/ArkhamDb") local spawnBag = require("playercards/SpawnBag") -- 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 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} -- ---------- IMPORTANT ---------- -- Coordinates defined below are in global dimensions relative to the panel - DO NOT USE THESE -- DIRECTLY. Call scalePositions() before use, and reference the variables below -- Layout width for a single card, in global coordinate space local CARD_WIDTH = 2.3 -- Coordinates to begin laying out cards. These vary based on the cards that are being placed by -- considering the width of the cards, number of cards, and desired spread intervals. -- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y -- coordinates on these provide global disances while the Z is local. local START_POSITIONS = { classCards = Vector(CARD_WIDTH * 9.5, 2, 1.4), investigator = Vector(6 * 2.5, 2, 1.3), cycle = Vector(CARD_WIDTH * 9.5, 2, 2.4), other = Vector(CARD_WIDTH * 9.5, 2, 1.4), randomWeakness = Vector(0, 2, 1.4), -- Because the card spread is handled by the SpawnBag, we don't know (programatically) where this -- should be placed. If more customizable cards are added it will need to be moved. summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7), } -- 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(0, 0, 11) local INVESTIGATOR_POSITION_SHIFT_COL = Vector(-6, 0, 0) 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(0, 0, 2.55) local INVESTIGATOR_SIGNATURE_OFFSET = Vector(0, 0, 5.75) -- USE THESE! Positions and offset shifts accounting for the scale of the panel local startPositions local cardRowOffset local cardGroupOffset local investigatorPositionShiftRow local investigatorPositionShiftCol local investigatorCardOffset local investigatorSignatureOffset 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", "The Feast of Hemlock Vale", "Investigator Packs" } local excludedNonBasicWeaknesses local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY local helpVisibleToPlayers = { } 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 buildExcludedWeaknessList() createButtons() end -- Build a list of non-basic weaknesses which should be excluded from the last weakness set, -- including all signature cards and evolved weaknesses. function buildExcludedWeaknessList() excludedNonBasicWeaknesses = { } for _, investigator in pairs(INVESTIGATORS) do for _, signatureId in ipairs(investigator.signatures) do excludedNonBasicWeaknesses[signatureId] = true end end for _, weaknessId in ipairs(EVOLVED_WEAKNESSES) do excludedNonBasicWeaknesses[weaknessId] = true end end function createButtons() createHelpButton() 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 createHelpButton() self.createButton({ function_owner = self, click_function = "toggleHelp", position = Vector(0.845, 0.1, -0.855), rotation = Vector(0, 0, 0), height = 180, width = 180, scale = Vector(0.25, 1, 0.25), color = TRANSPARENT, }) 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 = "All Weaknesses" weaknessButtonParams.position = buttonPos self.createButton(weaknessButtonParams) buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET weaknessButtonParams.click_function = "spawnRandomWeakness" weaknessButtonParams.tooltip = "Random Basic 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 two centered buttons on the final row buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2 --[[ 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 toggleHelp(_, playerColor, _) if helpVisibleToPlayers[playerColor] then helpVisibleToPlayers[playerColor] = nil else helpVisibleToPlayers[playerColor] = true end updateHelpVisibility() end function updateHelpVisibility() local visibility = "" for player, _ in pairs(helpVisibleToPlayers) do if string.len(visibility) > 0 then visibility = visibility .. "|" .. player else visibility = player end end self.UI.setAttribute("helpText", "visibility", visibility) self.UI.setAttribute("helpPanel", "visibility", visibility) self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0) 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 -- Clears the table and updates positions based on scale. Should be called before ANY card -- placement function prepareToPlaceCards() deleteAll() scalePositions() end -- Updates the positions based on the current object scale to ensure the relative layout functions -- properly at different scales. function scalePositions() -- Assume scaling is consistent in X and Z dimensions local scale = 1 / self.getScale().x startPositions = { } for key, pos in pairs(START_POSITIONS) do -- Because a scaled object means a different global size, using global distance for Z results in -- the cards being closer or farther depending on the scale. Leave the Z values and only scale -- X and Y startPositions[key] = Vector(pos) startPositions[key].x = startPositions[key].x * scale startPositions[key].y = startPositions[key].y * scale end cardRowOffset = CARD_ROW_OFFSET * scale cardGroupOffset = CARD_GROUP_OFFSET * scale investigatorPositionShiftRow = Vector(INVESTIGATOR_POSITION_SHIFT_ROW):scale(scale) investigatorPositionShiftCol = Vector(INVESTIGATOR_POSITION_SHIFT_COL):scale(scale) investigatorCardOffset = Vector(INVESTIGATOR_CARD_OFFSET):scale(scale) investigatorSignatureOffset = Vector(INVESTIGATOR_SIGNATURE_OFFSET):scale(scale) 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 prepareToPlaceCards() 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) if INVESTIGATOR_GROUPS[groupName] == nil then printToAll("No " .. groupName .. " data yet") return end local col = 1 local row = 1 local investigatorCount = #INVESTIGATOR_GROUPS[groupName] local position = getInvestigatorRowStartPos(investigatorCount, row) for i, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec( investigatorName, INVESTIGATORS[investigatorName], position, false)) do spawnBag.spawn(spawnSpec) end position:add(investigatorPositionShiftCol) col = col + 1 if col > INVESTIGATOR_MAX_COLS then col = 1 row = row + 1 position = getInvestigatorRowStartPos(investigatorCount, row) end end end function getInvestigatorRowStartPos(investigatorCount, row) local rowStart = Vector(startPositions.investigator) rowStart:add(Vector( investigatorPositionShiftRow.x * (row - 1), investigatorPositionShiftRow.y * (row - 1), investigatorPositionShiftRow.z * (row - 1))) local investigatorsInRow = math.min(investigatorCount - INVESTIGATOR_MAX_COLS * (row - 1), INVESTIGATOR_MAX_COLS) rowStart:add(Vector( investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2)) return rowStart 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(investigatorSignatureOffset) local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position) table.insert(spawns, { name = investigatorName.."signatures", cards = investigatorData.signatures, globalPos = self.positionToWorld(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(investigatorCardOffset) return { { name = investigatorName.."minicards", cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards, globalPos = self.positionToWorld(position), rotation = FACE_UP_ROTATION, }, { name = investigatorName.."cards", cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards, globalPos = self.positionToWorld(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 col = 1 local row = 1 local investigatorCount = #INVESTIGATOR_GROUPS[groupName] local position = getInvestigatorRowStartPos(investigatorCount, row) for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do spawnStarterDeck(investigatorName, INVESTIGATORS[investigatorName], position) position:add(investigatorPositionShiftCol) col = col + 1 if col > INVESTIGATOR_MAX_COLS then col = 1 row = row + 1 position = getInvestigatorRowStartPos(investigatorCount, row) 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(investigatorSignatureOffset) 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 = self.positionToWorld(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) prepareToPlaceCards() 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 indexReady = allCardsBagApi.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 = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded) local skillList = { } local eventList = { } local assetList = { } for _, cardId in ipairs(cardIdList) do local cardMetadata = allCardsBagApi.getCardById(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(startPositions.classCards) if #skillList > 0 then spawnBag.spawn({ name = cardClass .. (isUpgraded and "upgraded" or "basic"), cards = skillList, globalPos = self.positionToWorld(groupPos), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) groupPos.z = groupPos.z + math.ceil(#skillList / 20) * cardRowOffset + cardGroupOffset end if #eventList > 0 then spawnBag.spawn({ name = cardClass .. "event" .. (isUpgraded and "upgraded" or "basic"), cards = eventList, globalPos = self.positionToWorld(groupPos), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) groupPos.z = groupPos.z + math.ceil(#eventList / 20) * cardRowOffset + cardGroupOffset end if #assetList > 0 then spawnBag.spawn({ name = cardClass .. "asset" .. (isUpgraded and "upgraded" or "basic"), cards = assetList, globalPos = self.positionToWorld(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) prepareToPlaceCards() spawnInvestigators(cycle) local indexReady = allCardsBagApi.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 = allCardsBagApi.getCardsByCycle(cycle) local copiedList = { } for i, id in ipairs(cycleCardList) do copiedList[i] = id end spawnBag.spawn({ name = "cycle"..cycle, cards = copiedList, globalPos = self.positionToWorld(startPositions.cycle), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) end function spawnBonded() prepareToPlaceCards() spawnBag.spawn({ name = "bonded", cards = BONDED_CARD_LIST, globalPos = self.positionToWorld(startPositions.classCards), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) end function spawnUpgradeSheets() prepareToPlaceCards() spawnBag.spawn({ name = "upgradeSheets", cards = UPGRADE_SHEET_LIST, globalPos = self.positionToWorld(startPositions.classCards), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) spawnBag.spawn({ name = "servitor", cards = { "09080-m" }, globalPos = self.positionToWorld(startPositions.summonedServitor), rotation = FACE_UP_ROTATION, }) end -- Clears the current cards, and places all basic weaknesses on the table. function spawnWeaknesses() prepareToPlaceCards() local indexReady = allCardsBagApi.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 = allCardsBagApi.getUniqueWeaknesses() local basicWeaknessList = { } local otherWeaknessList = { } for i, id in ipairs(weaknessIdList) do local cardMetadata = allCardsBagApi.getCardById(id).metadata if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then table.insert(basicWeaknessList, id) elseif excludedNonBasicWeaknesses[id] == nil then table.insert(otherWeaknessList, id) end end local groupPos = Vector(startPositions.classCards) spawnBag.spawn({ name = "basicWeaknesses", cards = basicWeaknessList, globalPos = self.positionToWorld(groupPos), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) groupPos.z = groupPos.z + math.ceil(#basicWeaknessList / 20) * cardRowOffset + cardGroupOffset spawnBag.spawn({ name = "evolvedWeaknesses", cards = EVOLVED_WEAKNESSES, globalPos = self.positionToWorld(groupPos), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) groupPos.z = groupPos.z + math.ceil(#EVOLVED_WEAKNESSES / 20) * cardRowOffset + cardGroupOffset spawnBag.spawn({ name = "otherWeaknesses", cards = otherWeaknessList, globalPos = self.positionToWorld(groupPos), rotation = FACE_UP_ROTATION, spread = true, spreadCols = 20 }) end function spawnRandomWeakness() prepareToPlaceCards() local weaknessId = allCardsBagApi.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 = self.positionToWorld(startPositions.randomWeakness), rotation = FACE_UP_ROTATION, }) end