Merge branch 'main' into gamekeyhandler

This commit is contained in:
Chr1Z93 2024-05-29 10:09:36 +02:00
commit 1c138c750d
8 changed files with 446 additions and 335 deletions

View File

@ -22,6 +22,13 @@
"ImageURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940937086/92256BDF101E6272AD1E3F5F0043D311DF708F03/", "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940937086/92256BDF101E6272AD1E3F5F0043D311DF708F03/",
"WidthScale": 0 "WidthScale": 0
}, },
"CustomUIAssets": [
{
"Name": "OtherCards",
"Type": 0,
"URL": "http://cloud-3.steamusercontent.com/ugc/2446096169989812196/B5C491331EB348C261F561DC7A19968ECF9FC74A/"
}
],
"Description": "", "Description": "",
"DragSelectable": true, "DragSelectable": true,
"GMNotes": "", "GMNotes": "",
@ -53,5 +60,5 @@
"scaleZ": 10 "scaleZ": 10
}, },
"Value": 0, "Value": 0,
"XmlUI": "\u003cInclude src=\"playercards/PlayerCardPanel.xml\"/\u003e" "XmlUI": ""
} }

View File

@ -150,11 +150,8 @@ end
-- Event handlers for deck ID change -- Event handlers for deck ID change
function redDeckChanged(_, _, inputValue) redDeckId = inputValue end function redDeckChanged(_, _, inputValue) redDeckId = inputValue end
function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end
function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end
function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end
-- Event handlers for toggle buttons -- Event handlers for toggle buttons
@ -174,14 +171,7 @@ function loadInvestigatorsChanged()
end end
function loadDecks() function loadDecks()
-- testLoadLotsOfDecks() if not allCardsBagApi.isIndexReady() then return end
-- Method in DeckImporterMain, visible due to inclusion
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
if (redDeckId ~= nil and redDeckId ~= "") then if (redDeckId ~= nil and redDeckId ~= "") then
buildDeck("Red", redDeckId) buildDeck("Red", redDeckId)
end end

View File

@ -1,3 +1,5 @@
local guidReferenceApi = require("core/GUIDReferenceApi")
local cardIdIndex = {} local cardIdIndex = {}
local classAndLevelIndex = {} local classAndLevelIndex = {}
local basicWeaknessList = {} local basicWeaknessList = {}
@ -5,6 +7,7 @@ local uniqueWeaknessList = { }
local cycleIndex = {} local cycleIndex = {}
local indexingDone = false local indexingDone = false
local otherCardsDetected = false
function onLoad() function onLoad()
self.addContextMenuItem("Rebuild Index", startIndexBuild) self.addContextMenuItem("Rebuild Index", startIndexBuild)
@ -18,7 +21,7 @@ end
-- called once indexing is complete it means the hotfix bag has been added -- called once indexing is complete it means the hotfix bag has been added
-- later, and we should rebuild the index to integrate the hotfix bag. -- later, and we should rebuild the index to integrate the hotfix bag.
function rebuildIndexForHotfix() function rebuildIndexForHotfix()
if (indexingDone) then if indexingDone then
startIndexBuild() startIndexBuild()
end end
end end
@ -63,6 +66,7 @@ end
function buildIndex() function buildIndex()
local cardCount = 0 local cardCount = 0
indexingDone = false indexingDone = false
otherCardsDetected = false
-- process the allcardsbag itself -- process the allcardsbag itself
for _, cardData in ipairs(self.getData().ContainedObjects) do for _, cardData in ipairs(self.getData().ContainedObjects) do
@ -84,8 +88,8 @@ function buildIndex()
end end
for _, cardData in ipairs(hotfixData.ContainedObjects) do for _, cardData in ipairs(hotfixData.ContainedObjects) do
-- process containers
if cardData.ContainedObjects then if cardData.ContainedObjects then
-- process containers
for _, deepCardData in ipairs(cardData.ContainedObjects) do for _, deepCardData in ipairs(cardData.ContainedObjects) do
addCardToIndex(deepCardData) addCardToIndex(deepCardData)
cardCount = cardCount + 1 cardCount = cardCount + 1
@ -94,8 +98,8 @@ function buildIndex()
coroutine.yield(0) coroutine.yield(0)
end end
end end
-- process single cards
else else
-- process single cards
addCardToIndex(cardData) addCardToIndex(cardData)
cardCount = cardCount + 1 cardCount = cardCount + 1
if cardCount > 19 then if cardCount > 19 then
@ -108,6 +112,7 @@ function buildIndex()
end end
buildSupplementalIndexes() buildSupplementalIndexes()
updatePlayerCardPanel()
indexingDone = true indexingDone = true
return 1 return 1
end end
@ -116,15 +121,19 @@ end
---@param cardData table TTS object data for the card ---@param cardData table TTS object data for the card
function addCardToIndex(cardData) function addCardToIndex(cardData)
-- using the more efficient 'json.parse()' to speed this process up -- using the more efficient 'json.parse()' to speed this process up
local status, cardMetadata = pcall(function() json.parse(cardData.GMNotes) end) local status, cardMetadata = pcall(function() return json.parse(cardData.GMNotes) end)
-- if an error happens, fallback to the regular parser -- if an error happens, fallback to the regular parser
if status ~= true then if status ~= true or cardMetadata == nil then
log("Fast parser failed for " .. cardData.Nickname .. ", using old parser instead.")
cardMetadata = JSON.decode(cardData.GMNotes) cardMetadata = JSON.decode(cardData.GMNotes)
end end
-- if metadata was not valid JSON or empty, don't add the card -- if metadata was not valid JSON or empty, don't add the card
if not cardMetadata then return end if not cardMetadata == nil then
log("Error parsing " .. cardData.Nickname)
return
end
-- use the ZoopGuid as fallback if no id present -- use the ZoopGuid as fallback if no id present
cardMetadata.id = cardMetadata.id or cardMetadata.TtsZoopGuid cardMetadata.id = cardMetadata.id or cardMetadata.TtsZoopGuid
@ -138,37 +147,35 @@ function addCardToIndex(cardData)
end end
end end
-- Creates the supplemental indexes for classes, weaknesses etc.
function buildSupplementalIndexes() function buildSupplementalIndexes()
for cardId, card in pairs(cardIdIndex) do for cardId, card in pairs(cardIdIndex) do
local cardMetadata = card.metadata
-- If the ID key and the metadata ID don't match this is a duplicate card created by an alternate_id, and we should skip it -- If the ID key and the metadata ID don't match this is a duplicate card created by an alternate_id, and we should skip it
if cardId == cardMetadata.id then if cardId == card.metadata.id then
-- Add card to the basic weakness list, if appropriate. Some weaknesses have multiple copies, and are added multiple times -- Add card to the basic weakness list, if appropriate. Some weaknesses have multiple copies, and are added multiple times
if cardMetadata.weakness then if card.metadata.weakness then
table.insert(uniqueWeaknessList, cardMetadata.id) table.insert(uniqueWeaknessList, card.metadata.id)
if cardMetadata.basicWeaknessCount ~= nil then if card.metadata.basicWeaknessCount ~= nil then
for i = 1, cardMetadata.basicWeaknessCount do for i = 1, card.metadata.basicWeaknessCount do
table.insert(basicWeaknessList, cardMetadata.id) table.insert(basicWeaknessList, card.metadata.id)
end end
end end
end end
-- Excludes signature cards (which have no class or level) -- Excludes signature cards (which have no class or level)
if cardMetadata.class ~= nil and cardMetadata.level ~= nil then if card.metadata.class ~= nil and card.metadata.level ~= nil then
local upgradeKey local upgradeKey = "-level0"
if cardMetadata.level > 0 then if card.metadata.level > 0 then
upgradeKey = "-upgrade" upgradeKey = "-upgrade"
else
upgradeKey = "-level0"
end end
-- parse classes (separated by "|") and add the card to the appropriate class and level indices -- parse classes (separated by "|") and add the card to the appropriate class and level indices
for str in cardMetadata.class:gmatch("([^|]+)") do for str in card.metadata.class:gmatch("([^|]+)") do
table.insert(classAndLevelIndex[str .. upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex[str .. upgradeKey], card.metadata.id)
end end
-- add to cycle index -- add to cycle index
local cycleName = cardMetadata.cycle local cycleName = card.metadata.cycle
if cycleName ~= nil then if cycleName ~= nil then
cycleName = string.lower(cycleName) cycleName = string.lower(cycleName)
@ -177,12 +184,17 @@ function buildSupplementalIndexes()
-- override cycle name for night of the zealot -- override cycle name for night of the zealot
cycleName = cycleName:gsub("the night of the zealot", "core") cycleName = cycleName:gsub("the night of the zealot", "core")
else
-- track cards without defined cycle (should only be fan-made cards)
cycleName = "other"
otherCardsDetected = true
end
-- maybe initialize table
if cycleIndex[cycleName] == nil then if cycleIndex[cycleName] == nil then
cycleIndex[cycleName] = {} cycleIndex[cycleName] = {}
end end
table.insert(cycleIndex[cycleName], cardMetadata.id) table.insert(cycleIndex[cycleName], card.metadata.id)
end
end end
end end
end end
@ -216,64 +228,131 @@ function cardComparator(id1, id2)
end end
end end
-- inform the player card panel about the presence of other cards (no cycle -> fan-made)
function updatePlayerCardPanel()
local panel = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayerCardPanel")
panel.call("createXML", otherCardsDetected)
end
---@return boolean: If true, the bag is currently not indexing and ready to be accessed
function isIndexReady() function isIndexReady()
if not indexingDone then
broadcastToAll("Still loading player cards, please try again in a few seconds", { 0.9, 0.2, 0.2 })
end
return indexingDone return indexingDone
end end
-- Returns a specific card from the bag, based on ArkhamDB ID -- Returns a specific card from the bag, based on ArkhamDB ID
-- Params table: ---@param params table ID of the card to retrieve
-- id: String ID of the card to retrieve ---@return table: If the indexes are still being constructed, returns an empty table.
-- Return: If the indexes are still being constructed, an empty table is -- Otherwise, a single table with the following fields
-- returned. Otherwise, a single table with the following fields -- data: TTS object data, suitable for spawning the card
-- cardData: TTS object data, suitable for spawning the card -- metadata: Table of parsed metadata
-- cardMetadata: Table of parsed metadata
function getCardById(params) function getCardById(params)
if (not indexingDone) then if not isIndexReady() then return {} end
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return { }
end
return cardIdIndex[params.id] return cardIdIndex[params.id]
end end
-- Returns a list of cards from the bag matching a class and level (0 or upgraded) -- Returns a list of cards from the bag matching a class and level (0 or upgraded)
-- Params table: ---@param params table
-- class: String class to retrieve ("Guardian", "Seeker", etc) -- class: String class to retrieve ("Guardian", "Seeker", etc)
-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0 -- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0
-- Return: If the indexes are still being constructed, returns an empty table. ---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a list of tables, each with the following fields -- Otherwise, a list of tables, each with the following fields
-- cardData: TTS object data, suitable for spawning the card -- data: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata -- metadata: Table of parsed metadata
function getCardsByClassAndLevel(params) function getCardsByClassAndLevel(params)
if (not indexingDone) then if not isIndexReady() then return {} end
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return { } local upgradeKey = "-level0"
end if params.upgraded then
local upgradeKey
if (params.upgraded) then
upgradeKey = "-upgrade" upgradeKey = "-upgrade"
else
upgradeKey = "-level0"
end end
return classAndLevelIndex[params.class..upgradeKey]; return classAndLevelIndex[params.class .. upgradeKey]
end end
function getCardsByCycle(cycleName) -- Returns a list of cards from the bag matching a cycle
if (not indexingDone) then ---@param params table
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2}) -- cycle: String cycle to retrieve ("The Scarlet Keys" etc.)
return { } -- sortByMetadata: true to sort the table by metadata instead of ID
---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a list of tables, each with the following fields
-- data: TTS object data, suitable for spawning the card
-- metadata: Table of parsed metadata
function getCardsByCycle(params)
if not isIndexReady() then return {} end
if not params.sortByMetadata then
return cycleIndex[string.lower(params.cycle)]
end end
return cycleIndex[string.lower(cycleName)]
-- sort list by metadata (useful for custom cards without proper IDs)
local cardList = {}
for _, id in ipairs(cycleIndex[string.lower(params.cycle)]) do
table.insert(cardList, id)
end
table.sort(cardList, metadataSortFunction)
return cardList
end
-- sorts cards by metadata: class, type, level, name and then description
function metadataSortFunction(id1, id2)
local card1 = cardIdIndex[id1]
local card2 = cardIdIndex[id2]
-- extract class per card
local classValue1 = getClassValueFromString(card1.metadata.class)
local classValue2 = getClassValueFromString(card2.metadata.class)
-- conversion tables to simplify type sorting
local typeConversion = {
Asset = 1,
Event = 2,
Skill = 3
}
if classValue1 ~= classValue2 then
return classValue1 < classValue2
elseif typeConversion[card1.metadata.type] ~= typeConversion[card2.metadata.type] then
return typeConversion[card1.metadata.type] < typeConversion[card2.metadata.type]
elseif card1.metadata.level ~= card2.metadata.level then
return card1.metadata.level < card2.metadata.level
elseif card1.data.Nickname ~= card2.data.Nickname then
return card1.data.Nickname < card2.data.Nickname
else
return card1.data.Description < card2.data.Description
end
end
-- helper function to calculate the class value for sorting from the "|" separated string
function getClassValueFromString(s)
local classValueList = {
Guardian = 1,
Seeker = 2,
Rogue = 3,
Mystic = 4,
Survivor = 5,
Neutral = 6
}
local classValue = 0
for str in s:gmatch("([^|]+)") do
-- this sorts multiclass cards
classValue = classValue * 10 + classValueList[str]
end
return classValue
end end
-- Searches the bag for cards which match the given name and returns a list. Note that this is -- Searches the bag for cards which match the given name and returns a list. Note that this is
-- an O(n) search without index support. It may be slow. -- an O(n) search without index support. It may be slow.
-- Parameter array must contain these fields to define the search: -- Parameter array must contain these fields to define the search:
-- name String or string fragment to search for names -- name: String or string fragment to search for names
-- exact Whether the name match should be exact -- exact: Whether the name match should be exact
function getCardsByName(params) function getCardsByName(params)
local name = params.name local name = params.name
local exact = params.exact local exact = params.exact
local results = {} local results = {}
-- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs -- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs
local addedCards = {} local addedCards = {}
for _, cardData in pairs(cardIdIndex) do for _, cardData in pairs(cardIdIndex) do
@ -288,33 +367,30 @@ function getCardsByName(params)
return results return results
end end
-- Gets a random basic weakness from the bag. Once a given ID has been returned -- Gets a random basic weakness from the bag. Once a given ID has been returned it will be
-- it will be removed from the list and cannot be selected again until a reload -- removed from the list and cannot be selected again until a reload occurs or the indexes
-- occurs or the indexes are rebuilt, which will refresh the list to include all -- are rebuilt, which will refresh the list to include all weaknesses.
-- weaknesses. ---@return string: ID of the selected weakness
-- Return: String ID of the selected weakness.
function getRandomWeaknessId() function getRandomWeaknessId()
local availableWeaknesses = buildAvailableWeaknesses() local availableWeaknesses = buildAvailableWeaknesses()
if (#availableWeaknesses > 0) then if #availableWeaknesses > 0 then
return availableWeaknesses[math.random(#availableWeaknesses)] return availableWeaknesses[math.random(#availableWeaknesses)]
end end
end end
-- Constructs a list of available basic weaknesses by starting with the full pool of basic -- Constructs a list of available basic weaknesses by starting with the full pool of basic
-- weaknesses then removing any which are currently in the play or deck construction areas -- weaknesses then removing any which are currently in the play or deck construction areas
-- Return: Table array of weakness IDs which are valid to choose from ---@return table: Array of weakness IDs which are valid to choose from
function buildAvailableWeaknesses() function buildAvailableWeaknesses()
local weaknessesInPlay = {} local weaknessesInPlay = {}
local allObjects = getAllObjects() local allObjects = getAllObjects()
for _, object in ipairs(allObjects) do for _, object in ipairs(allObjects) do
if (object.name == "Deck") then if object.type == "Deck" then
for _, cardData in ipairs(object.getData().ContainedObjects) do for _, cardData in ipairs(object.getData().ContainedObjects) do
local cardMetadata = JSON.decode(cardData.GMNotes) incrementWeaknessCount(weaknessesInPlay, JSON.decode(cardData.GMNotes))
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
end end
elseif (object.name == "Card") then elseif object.type == "Card" then
local cardMetadata = JSON.decode(object.getGMNotes()) incrementWeaknessCount(weaknessesInPlay, JSON.decode(object.getGMNotes()))
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
end end
end end
@ -339,8 +415,8 @@ end
-- Helper function that adds one to the table entry for the number of weaknesses in play -- Helper function that adds one to the table entry for the number of weaknesses in play
function incrementWeaknessCount(table, cardMetadata) function incrementWeaknessCount(table, cardMetadata)
if (isBasicWeakness(cardMetadata)) then if isBasicWeakness(cardMetadata) then
if (table[cardMetadata.id] == nil) then if table[cardMetadata.id] == nil then
table[cardMetadata.id] = 1 table[cardMetadata.id] = 1
else else
table[cardMetadata.id] = table[cardMetadata.id] + 1 table[cardMetadata.id] = table[cardMetadata.id] + 1

View File

@ -6,22 +6,29 @@ do
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag") return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag")
end end
-- internal function to create a copy of the table to avoid operating on variables owned by different objects
local function returnCopyOfList(data)
local copiedList = {}
for _, id in ipairs(data) do
table.insert(copiedList, id)
end
return copiedList
end
-- Returns a specific card from the bag, based on ArkhamDB ID -- Returns a specific card from the bag, based on ArkhamDB ID
---@param id table String ID of the card to retrieve ---@param id string ID of the card to retrieve
---@return table table ---@return table: If the indexes are still being constructed, returns an empty table.
-- If the indexes are still being constructed, an empty table is -- Otherwise, a single table with the following fields
-- returned. Otherwise, a single table with the following fields -- data: TTS object data, suitable for spawning the card
-- cardData: TTS object data, suitable for spawning the card -- metadata: Table of parsed metadata
-- cardMetadata: Table of parsed metadata
AllCardsBagApi.getCardById = function(id) AllCardsBagApi.getCardById = function(id)
return getAllCardsBag().call("getCardById", { id = id }) return getAllCardsBag().call("getCardById", { id = id })
end end
-- Gets a random basic weakness from the bag. Once a given ID has been returned -- Gets a random basic weakness from the bag. Once a given ID has been returned it
-- it will be removed from the list and cannot be selected again until a reload -- will be removed from the list and cannot be selected again until a reload occurs
-- occurs or the indexes are rebuilt, which will refresh the list to include all -- or the indexes are rebuilt, which will refresh the list to include all weaknesses.
-- weaknesses. ---@return string: ID of the selected weakness
---@return string: ID of the selected weakness.
AllCardsBagApi.getRandomWeaknessId = function() AllCardsBagApi.getRandomWeaknessId = function()
return getAllCardsBag().call("getRandomWeaknessId") return getAllCardsBag().call("getRandomWeaknessId")
end end
@ -36,15 +43,15 @@ do
-- called once indexing is complete it means the hotfix bag has been added -- called once indexing is complete it means the hotfix bag has been added
-- later, and we should rebuild the index to integrate the hotfix bag. -- later, and we should rebuild the index to integrate the hotfix bag.
AllCardsBagApi.rebuildIndexForHotfix = function() AllCardsBagApi.rebuildIndexForHotfix = function()
return getAllCardsBag().call("rebuildIndexForHotfix") getAllCardsBag().call("rebuildIndexForHotfix")
end end
-- Searches the bag for cards which match the given name and returns a list. Note that this is -- Searches the bag for cards which match the given name and returns a list.
-- an O(n) search without index support. It may be slow. -- Note that this is an O(n) search without index support. It may be slow.
---@param name string or string fragment to search for names ---@param name string or string fragment to search for names
---@param exact boolean Whether the name match should be exact ---@param exact boolean Whether the name match should be exact
AllCardsBagApi.getCardsByName = function(name, exact) AllCardsBagApi.getCardsByName = function(name, exact)
return getAllCardsBag().call("getCardsByName", {name = name, exact = exact}) return returnCopyOfList(getAllCardsBag().call("getCardsByName", { name = name, exact = exact }))
end end
AllCardsBagApi.isBagPresent = function() AllCardsBagApi.isBagPresent = function()
@ -53,21 +60,28 @@ do
-- Returns a list of cards from the bag matching a class and level (0 or upgraded) -- Returns a list of cards from the bag matching a class and level (0 or upgraded)
---@param class string class to retrieve ("Guardian", "Seeker", etc) ---@param class string class to retrieve ("Guardian", "Seeker", etc)
---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0 ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0
---@return table: If the indexes are still being constructed, returns an empty table. ---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a list of tables, each with the following fields -- Otherwise, a list of tables, each with the following fields
-- cardData: TTS object data, suitable for spawning the card -- data: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata -- metadata: Table of parsed metadata
AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)
return getAllCardsBag().call("getCardsByClassAndLevel", {class = class, upgraded = upgraded}) return returnCopyOfList(getAllCardsBag().call("getCardsByClassAndLevel", { class = class, upgraded = upgraded }))
end end
AllCardsBagApi.getCardsByCycle = function(cycle) -- Returns a list of cards from the bag matching a cycle
return getAllCardsBag().call("getCardsByCycle", cycle) ---@param cycle string Cycle to retrieve ("The Scarlet Keys" etc.)
---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID
---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a list of tables, each with the following fields
-- data: TTS object data, suitable for spawning the card
-- metadata: Table of parsed metadata
AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)
return returnCopyOfList(getAllCardsBag().call("getCardsByCycle", { cycle = cycle, sortByMetadata = sortByMetadata }))
end end
AllCardsBagApi.getUniqueWeaknesses = function() AllCardsBagApi.getUniqueWeaknesses = function()
return getAllCardsBag().call("getUniqueWeaknesses") return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses"))
end end
return AllCardsBagApi return AllCardsBagApi

View File

@ -26,8 +26,6 @@ local CYCLE_BUTTONS_Z_OFFSET = 0.2665
local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 } local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }
local TRANSPARENT = { 0, 0, 0, 0 } 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_UP_ROTATION = { x = 0, y = 270, z = 0 }
local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180 } local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180 }
@ -78,7 +76,14 @@ local investigatorPositionShiftCol
local investigatorCardOffset local investigatorCardOffset
local investigatorSignatureOffset local investigatorSignatureOffset
local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" } local CLASS_LIST = {
"Guardian",
"Seeker",
"Rogue",
"Mystic",
"Survivor",
"Neutral"
}
local CYCLE_LIST = { local CYCLE_LIST = {
"Core", "Core",
"The Dunwich Legacy", "The Dunwich Legacy",
@ -94,8 +99,7 @@ local CYCLE_LIST = {
} }
local excludedNonBasicWeaknesses local excludedNonBasicWeaknesses
local spawnStarterDecks = false
local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
local helpVisibleToPlayers = {} local helpVisibleToPlayers = {}
function onSave() function onSave()
@ -274,9 +278,8 @@ function createCycleButtons()
if rowCount == 3 then if rowCount == 3 then
-- Account for two centered buttons on the final row -- Account for two centered buttons on the final row
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2 buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2
--[[ Account for centered button on the final row -- Account for centered button on the final row
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET -- buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
]]
end end
else else
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
@ -297,8 +300,6 @@ function createClearButton()
end end
function createInvestigatorModeButtons() function createInvestigatorModeButtons()
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
self.createButton({ self.createButton({
function_owner = self, function_owner = self,
click_function = "setCardsOnlyMode", click_function = "setCardsOnlyMode",
@ -306,18 +307,18 @@ function createInvestigatorModeButtons()
height = 170, height = 170,
width = 760, width = 760,
scale = Vector(0.25, 1, 0.25), scale = Vector(0.25, 1, 0.25),
color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR color = spawnStarterDecks and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR
}) })
self.createButton({ self.createButton({
function_owner = self, function_owner = self,
click_function = "setStarterDeckMode", click_function = "setspawnStarterDecks",
position = Vector(0.66, 0.1, -0.322), position = Vector(0.66, 0.1, -0.322),
height = 170, height = 170,
width = 760, width = 760,
scale = Vector(0.25, 1, 0.25), scale = Vector(0.25, 1, 0.25),
color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT color = spawnStarterDecks and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT
}) })
local checkX = starterMode and 0.52 or 0.11 local checkX = spawnStarterDecks and 0.52 or 0.11
self.createButton({ self.createButton({
function_owner = self, function_owner = self,
label = "✓", label = "✓",
@ -325,12 +326,77 @@ function createInvestigatorModeButtons()
position = Vector(checkX, 0.11, -0.317), position = Vector(checkX, 0.11, -0.317),
height = 0, height = 0,
width = 0, width = 0,
scale = Vector(0.3, 1, 0.3), font_size = 300,
scale = Vector(0.1, 1, 0.1),
font_color = { 0, 0, 0 }, font_color = { 0, 0, 0 },
color = { 1, 1, 1 } color = { 1, 1, 1 }
}) })
end end
function createXML(showOtherCardsButton)
-- basic XML for the help button
local xmlTable = {
{
tag = "Panel",
attributes = {
active = "false",
id = "helpPanel",
position = "-165 -70 -2",
rotation = "0 0 180",
height = "50",
width = "107",
color = "#00000099"
},
children = {
tag = "Text",
attributes = {
id = "helpText",
rectAlignment = "MiddleCenter",
height = "480",
width = "1000",
scale = "0.1 0.1 1",
fontSize = "66",
color = "#F5F5F5",
backgroundColor = "#FF0000",
alignment = "MiddleLeft",
horizontalOverflow = "wrap",
text = "• Select a group to place cards\n" ..
"• Copy the cards you want for your deck\n" ..
"• Select a new group to clear the placed cards and see new ones\n" ..
"• Clear to remove all cards"
}
}
}
}
-- add the "Additional Cards" button if cards without cycle were detected
if showOtherCardsButton then
local otherCardsButtonXml = {
tag = "Panel",
attributes = {
position = "44.25 65.75 -11",
rotation = "0 0 180",
height = "225",
width = "225",
scale = "0.1 0.1 1",
onClick = "spawnOtherCards"
},
children = {
tag = "Image",
attributes = { image = "OtherCards" }
}
}
table.insert(xmlTable, otherCardsButtonXml)
end
helpVisibleToPlayers = {}
self.UI.setXmlTable(xmlTable)
end
-- click function for the XML button for the additional player cards
function spawnOtherCards()
spawnCycle("Other")
end
function toggleHelp(_, playerColor, _) function toggleHelp(_, playerColor, _)
if helpVisibleToPlayers[playerColor] then if helpVisibleToPlayers[playerColor] then
helpVisibleToPlayers[playerColor] = nil helpVisibleToPlayers[playerColor] = nil
@ -354,13 +420,13 @@ function updateHelpVisibility()
self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0) self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0)
end end
function setStarterDeckMode() function setspawnStarterDecks()
starterDeckMode = STARTER_DECK_MODE_STARTERS spawnStarterDecks = true
updateStarterModeButtons() updateStarterModeButtons()
end end
function setCardsOnlyMode() function setCardsOnlyMode()
starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY spawnStarterDecks = false
updateStarterModeButtons() updateStarterModeButtons()
end end
@ -405,14 +471,12 @@ function deleteAll()
spawnBag.recall(true) spawnBag.recall(true)
end end
-- Spawn an investigator group, based on the current UI setting for either investigators or starter -- Spawn an investigator group, based on the current UI setting for either investigators or starter decks
-- decks.
---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData ---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnInvestigatorGroup(groupName) function spawnInvestigatorGroup(groupName)
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
prepareToPlaceCards() prepareToPlaceCards()
Wait.frames(function() Wait.frames(function()
if starterMode then if spawnStarterDecks then
spawnStarters(groupName) spawnStarters(groupName)
else else
spawnInvestigators(groupName) spawnInvestigators(groupName)
@ -425,7 +489,7 @@ end
---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData ---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnInvestigators(groupName) function spawnInvestigators(groupName)
if INVESTIGATOR_GROUPS[groupName] == nil then if INVESTIGATOR_GROUPS[groupName] == nil then
printToAll("No " .. groupName .. " data yet") printToAll("No investigator data for " .. groupName .. " yet")
return return
end end
@ -434,7 +498,7 @@ function spawnInvestigators(groupName)
local investigatorCount = #INVESTIGATOR_GROUPS[groupName] local investigatorCount = #INVESTIGATOR_GROUPS[groupName]
local position = getInvestigatorRowStartPos(investigatorCount, row) local position = getInvestigatorRowStartPos(investigatorCount, row)
for i, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do
for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do
spawnBag.spawn(spawnSpec) spawnBag.spawn(spawnSpec)
end end
@ -547,6 +611,7 @@ function spawnStarterDeck(investigatorName, investigatorData, position)
}) })
end) end)
end end
-- Clears the currently placed cards, then places cards for the given class and level spread -- 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 cardClass string Class to place ("Guardian", "Seeker", etc)
---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. ---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.
@ -559,11 +624,8 @@ end
---@param cardClass string Class to place ("Guardian", "Seeker", etc) ---@param cardClass string Class to place ("Guardian", "Seeker", etc)
---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. ---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.
function placeClassCards(cardClass, isUpgraded) function placeClassCards(cardClass, isUpgraded)
local indexReady = allCardsBagApi.isIndexReady() if not allCardsBagApi.isIndexReady() then return end
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 cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded)
local skillList = {} local skillList = {}
@ -617,21 +679,20 @@ end
-- Spawns the investigator sets and all cards for the given cycle -- 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 ---@param cycle string Name of a cycle, should match the standard used in card metadata
function spawnCycle(cycle) function spawnCycle(cycle)
if not allCardsBagApi.isIndexReady() then return end
prepareToPlaceCards() prepareToPlaceCards()
spawnInvestigators(cycle) spawnInvestigators(cycle)
local indexReady = allCardsBagApi.isIndexReady()
if (not indexReady) then -- sort custom cards
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2}) local sortByMetadata = false
return if cycle == "Other" then
end sortByMetadata = true
local cycleCardList = allCardsBagApi.getCardsByCycle(cycle)
local copiedList = { }
for i, id in ipairs(cycleCardList) do
copiedList[i] = id
end end
spawnBag.spawn({ spawnBag.spawn({
name = "cycle" .. cycle, name = "cycle" .. cycle,
cards = copiedList, cards = allCardsBagApi.getCardsByCycle(cycle, sortByMetadata),
globalPos = self.positionToWorld(startPositions.cycle), globalPos = self.positionToWorld(startPositions.cycle),
rotation = FACE_UP_ROTATION, rotation = FACE_UP_ROTATION,
spread = true, spread = true,
@ -671,16 +732,13 @@ end
-- Clears the current cards, and places all basic weaknesses on the table. -- Clears the current cards, and places all basic weaknesses on the table.
function spawnWeaknesses() function spawnWeaknesses()
if not allCardsBagApi.isIndexReady() then return end
prepareToPlaceCards() 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 basicWeaknessList = {}
local otherWeaknessList = {} local otherWeaknessList = {}
for i, id in ipairs(weaknessIdList) do for _, id in ipairs(allCardsBagApi.getUniqueWeaknesses()) do
local cardMetadata = allCardsBagApi.getCardById(id).metadata local cardMetadata = allCardsBagApi.getCardById(id).metadata
if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then
table.insert(basicWeaknessList, id) table.insert(basicWeaknessList, id)

View File

@ -16,7 +16,7 @@ Spawner = { }
---@param sort boolean True if this list of cards should be sorted before spawning ---@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. ---@param callback? function Callback to be called after the card/deck spawns.
Spawner.spawnCards = function(cardList, pos, rot, sort, callback) Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
if (sort) then if sort then
table.sort(cardList, Spawner.cardComparator) table.sort(cardList, Spawner.cardComparator)
end end
@ -25,9 +25,9 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
local investigatorCards = { } local investigatorCards = { }
for _, card in ipairs(cardList) do for _, card in ipairs(cardList) do
if (card.metadata.type == "Investigator") then if card.metadata.type == "Investigator" then
table.insert(investigatorCards, card) table.insert(investigatorCards, card)
elseif (card.metadata.type == "Minicard") then elseif card.metadata.type == "Minicard" then
table.insert(miniCards, card) table.insert(miniCards, card)
else else
table.insert(standardCards, card) table.insert(standardCards, card)
@ -46,7 +46,7 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
end end
Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback) Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)
if (sort) then if sort then
table.sort(cardList, Spawner.cardComparator) table.sort(cardList, Spawner.cardComparator)
end end
@ -201,7 +201,7 @@ end
---@return string id >= startId ---@return string id >= startId
Spawner.findNextAvailableId = function(objectTable, startId) Spawner.findNextAvailableId = function(objectTable, startId)
local id = startId local id = startId
while (objectTable[id] ~= nil) do while objectTable[id] ~= nil do
id = tostring(tonumber(id) + 1) id = tostring(tonumber(id) + 1)
end end
return id return id

View File

@ -81,24 +81,16 @@ do
-- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples -- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples
SpawnBag.spawn = function(spawnSpec) SpawnBag.spawn = function(spawnSpec)
-- Limit to one placement at a time -- Limit to one placement at a time
if (placedSpecs[spawnSpec.name]) then if placedSpecs[spawnSpec.name] or spawnSpec == nil then return end
return
end
if (spawnSpec == nil) then
-- TODO: error here
return
end
local cardsToSpawn = {} local cardsToSpawn = {}
local cardList = spawnSpec.cards for _, cardId in ipairs(spawnSpec.cards) do
for _, cardId in ipairs(cardList) do local card = allCardsBagApi.getCardById(cardId)
local cardData = allCardsBagApi.getCardById(cardId) if card ~= nil then
if (cardData ~= nil) then table.insert(cardsToSpawn, card)
table.insert(cardsToSpawn, cardData)
else
-- TODO: error here
end end
end end
if (spawnSpec.spread) then if spawnSpec.spread then
Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject) Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject)
else else
-- TTS decks come out in reverse order of the cards, reverse the list so the input order stays -- TTS decks come out in reverse order of the cards, reverse the list so the input order stays
@ -120,14 +112,13 @@ do
internal.recallSpawned() internal.recallSpawned()
end end
-- We've recalled everything we can, some cards may have been moved out of the -- We've recalled everything we can, some cards may have been moved out of the card area. Just reset at this point.
-- card area. Just reset at this point.
placedSpecs = {} placedSpecs = {}
placedObjectGuids = {} placedObjectGuids = {}
recallZone = nil recallZone = nil
end end
-- Deleted all spawned cards. -- Delete all spawned cards
internal.deleteSpawned = function() internal.deleteSpawned = function()
for guid, _ in pairs(placedObjectGuids) do for guid, _ in pairs(placedObjectGuids) do
local obj = getObjectFromGUID(guid) local obj = getObjectFromGUID(guid)
@ -140,7 +131,7 @@ do
end end
end end
-- Recalls spawned cards with a fake bag that replicates the memory bag recall style. -- Recalls spawned cards with a fake bag that replicates the memory bag recall style
internal.recallSpawned = function() internal.recallSpawned = function()
local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() }) local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() })
for guid, _ in pairs(placedObjectGuids) do for guid, _ in pairs(placedObjectGuids) do
@ -156,9 +147,7 @@ do
trash.destruct() trash.destruct()
end end
-- Callback for when an object has been spawned. Tracks the object for later recall and updates the recall zone.
-- Callback for when an object has been spawned. Tracks the object for later recall and updates the
-- recall zone.
internal.recordPlacedObject = function(spawned) internal.recordPlacedObject = function(spawned)
placedObjectGuids[spawned.getGUID()] = true placedObjectGuids[spawned.getGUID()] = true
internal.expandRecallZone(spawned) internal.expandRecallZone(spawned)
@ -174,21 +163,22 @@ do
recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z } 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 } recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z }
return return
else end
if (pos.x > recallZone.upperLeft.x) then
if pos.x > recallZone.upperLeft.x then
recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X
end end
if (pos.x < recallZone.lowerRight.x) then if pos.x < recallZone.lowerRight.x then
recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X
end end
if (pos.z > recallZone.upperLeft.z) then if pos.z > recallZone.upperLeft.z then
recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z
end end
if (pos.z < recallZone.lowerRight.z) then if pos.z < recallZone.lowerRight.z then
recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z
end end
end
if (SHOW_RECALL_ZONE) then if SHOW_RECALL_ZONE then
local y = 1.5 local y = 1.5
local thick = 0.05 local thick = 0.05
Global.setVectorLines({ Global.setVectorLines({

View File

@ -1,24 +0,0 @@
<Panel
active="false"
id="helpPanel"
position="-165 -60 -2"
rotation="0 0 180"
height="55"
width="107"
color="#00000099">
<Text
id="helpText"
rectAlignment="MiddleCenter"
height="490"
width="1000"
scale="0.1 0.1 1"
fontSize="66"
color="#F5F5F5"
backgroundColor="#FF0000"
alignment="UpperLeft"
horizontalOverflow="wrap">
• Select a group to place cards
• Copy the cards you want for your deck
• Select a new group to clear the placed cards and see new ones
• Clear to remove all cards</Text>
</Panel>