Merge branch 'main' into hand-helper

This commit is contained in:
Chr1Z93 2024-05-29 10:09:24 +02:00
commit 11b3b332c5
23 changed files with 630 additions and 437 deletions

View File

@ -20,6 +20,7 @@
"Note": "",
"ObjectStates_order": [
"GUIDReferenceHandler.123456",
"GameKeyHandler.fce69c",
"TokenSpawnTracker.e3ffc9",
"HandTrigger.5fe087",
"HandTrigger.be2f17",
@ -196,7 +197,6 @@
"Fan-MadeExpansionOverview.de7cae",
"OptionPanelSource.830bd0",
"SoundCube.3c988f",
"GameKeyHandler.fce69c",
"TokenSpawningReference.f8b3a7",
"3DText.d628cc",
"NavigationOverlayHandler.797ede",

View File

@ -1,6 +1,6 @@
{
"1": {
"body": "Welcome to Arkham Horror LCG - Super Complete Edition!\n\nMake sure to take the tour that can be started with the token in the middle of the main playarea. Some basic notes:\n\nDECKBUILDING\n- All currently existing investigators and player cards are accessible via the player card panel in the upper left corner of the table.\n\n- On the leftside underneath the Investigators, you will find the ArkhamDB Deckimporter. Insert your deck ID and it will build the deck automatically for you.\n\nSCENARIOS & SETUP\n- Arkham Horror LCG comes with a core campaign (Night of the Zealot) and several expansions. Within each box you will find all the cards required for each scenario setup, as well as a the official campaign guide PDF.\n\n2. Each scenario is setup differently, and while some of the work has been prepared beforehand (such as building encounter decks), you will have to refer to the Campaign Guide for specific instructions on how to set up each scenario.\n\nINVESTIGATOR PLAYMAT AND GAMEPLAY\n- Playermats are scripted to automate most of the gameplay for you.",
"body": "Welcome to Arkham Horror LCG - Super Complete Edition!\n\nMake sure to take the tour that can be started with the token in the middle of the main playarea. Some basic notes:\n\nDECKBUILDING\n- All currently existing investigators and player cards are accessible via the player card panel in the upper left corner of the table.\n\n- On the leftside underneath the Investigators, you will find the ArkhamDB Deckimporter. Insert your deck ID and it will build the deck automatically for you.\n\nSCENARIOS \u0026 SETUP\n- Arkham Horror LCG comes with a core campaign (Night of the Zealot) and several expansions. Within each box you will find all the cards required for each scenario setup, as well as a the official campaign guide PDF.\n\n2. Each scenario is setup differently, and while some of the work has been prepared beforehand (such as building encounter decks), you will have to refer to the Campaign Guide for specific instructions on how to set up each scenario.\n\nINVESTIGATOR PLAYMAT AND GAMEPLAY\n- Playermats are scripted to automate most of the gameplay for you.",
"color": "Grey",
"id": 1,
"title": "Basic Intro",

View File

@ -33,7 +33,7 @@
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScript": "do_not_ready = true",
"LuaScript": "",
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Card",
@ -43,6 +43,7 @@
"Sticky": true,
"Tags": [
"Asset",
"DoNotReady",
"PlayerCard"
],
"Tooltip": true,

View File

@ -53,9 +53,9 @@
"rotX": 0,
"rotY": 180,
"rotZ": 0,
"scaleX": 1,
"scaleX": 1.15,
"scaleY": 1,
"scaleZ": 1
"scaleZ": 1.15
},
"Value": 0,
"XmlUI": ""

View File

@ -34,7 +34,7 @@
"LayoutGroupSortIndex": 0,
"Locked": true,
"LuaScript": "require(\"util/ConnectionDrawingTool\")",
"LuaScriptState": "{\"e8e04b\":[]}",
"LuaScriptState": "{\"connections\":[]}",
"MeasureMovement": false,
"Name": "Custom_Token",
"Nickname": "Drawing Tool",

View File

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

View File

@ -1 +1 @@
{"includeDrawnTokens":true,"percentage":false,"tokenPrecedence":{"":[0,11],"Auto-fail":[-100,7],"Bless":[101,8],"Cultist":[-2,4],"Curse":[-101,9],"Elder Sign":[100,2],"Elder Thing":[-4,6],"Frost":[-99,10],"Skull":[-1,3],"Tablet":[-3,5]}}
{"includeDrawnTokens":true,"percentage":false,"tokenPrecedence":{"":[0,11],"Auto-fail":[-100,7],"Bless":[110,8],"Cultist":[-2,4],"Curse":[-110,9],"Elder Sign":[100,2],"Elder Thing":[-4,6],"Frost":[-105,10],"Skull":[-1,3],"Tablet":[-3,5]}}

View File

@ -161,12 +161,12 @@ function restoreCampaignData(importData, coin)
deckImporterApi.setUiState(importData["decks"])
end
-- maybe set campaign guide page
if importData["guide"] then
-- maybe set campaign guide page (unless it was on the first page)
if importData["guide"] and importData["guide"] ~= 0 then
local campaignGuide = findUniqueObjectWithTag("CampaignGuide")
if campaignGuide then
Wait.condition(
-- Called after the condition function returns true
-- Called after the condition function returns true
function() printToAll("Campaign Guide import successful!") end,
-- Condition function that is called continuously until it returns true or timeout is reached
function() return campaignGuide.Book.setPage(importData["guide"]) end,
@ -190,6 +190,13 @@ function restoreCampaignData(importData, coin)
playAreaApi.updateSurface(importData["playarea"])
playAreaApi.setInvestigatorCount(importData["clueCount"])
-- restore Playmat slots
if importData["slotData"] then
for matColor, slotData in pairs(importData["slotData"]) do
playmatApi.loadSlotData(matColor, slotData)
end
end
coin.destruct()
broadcastToAll("Campaign successfully imported!", "Green")
end
@ -265,6 +272,13 @@ function createCampaignToken(_, playerColor, _)
table.insert(campaignTokenData.ContainedObjects, indexData)
end
-- get the slot symbol data for each playmat (use GUIDReferenceApi to only get this for existing playmats)
campaignData.slotData = {}
for matColor, _ in pairs(guidReferenceApi.getObjectsByType("Playermat")) do
local slotData = playmatApi.getSlotData(matColor)
campaignData.slotData[matColor] = slotData
end
-- finish the data for the campaign token
campaignTokenData.GMNotes = JSON.encode(campaignData)
campaignTokenData.Nickname = campaignBox.getName() .. os.date(" %b %d") .. " Save"

View File

@ -11,7 +11,8 @@ local phaseImages = {
"http://cloud-3.steamusercontent.com/ugc/982233321870237261/C287CAED2423970F33E72D6C7415CBEC6794C533/"
}
local phaseId, broadcastChange
-- these are intentionally global for remote updating
-- phaseId, broadcastChange
function onSave()
return JSON.encode({

View File

@ -12,16 +12,18 @@ buttonParameters.height = 325
local inputParameters = {}
inputParameters.function_owner = self
inputParameters.font_size = 100
inputParameters.width = 250
inputParameters.height = inputParameters.font_size + 23
inputParameters.font_size = 200
inputParameters.width = 500
inputParameters.height = inputParameters.font_size + 46
inputParameters.alignment = 3
inputParameters.validation = 2
inputParameters.tab = 2
inputParameters.scale = { 0.5, 0.5, 0.5 }
local percentageLabel = {}
percentageLabel.function_owner = self
percentageLabel.click_function = "none"
percentageLabel.font_size = 200
percentageLabel.width = 0
percentageLabel.height = 0
@ -69,13 +71,13 @@ function onLoad(savedData)
end
createButtonsAndInputs()
-- maybe trigger layout() to draw percentage buttons
local objList = getObjectsWithTag("tempToken")
if #objList > 0 then
Wait.time(layout, 0.5)
end
-- context menu items
self.addContextMenuItem("Load default values", function()
loadDefaultValues()
@ -153,10 +155,10 @@ function loadDefaultValues()
["Tablet"] = { -3, 5},
["Elder Thing"] = { -4, 6},
["Auto-fail"] = { -100, 7},
["Bless"] = { 101, 8},
["Curse"] = { -101, 9},
["Frost"] = { -99, 10},
[""] = { 0, 11}
["Bless"] = { 110, 8},
["Curse"] = { -110, 9},
["Frost"] = { -105, 10},
[""] = { 0, 11}
}
end
@ -195,9 +197,11 @@ function createButtonsAndInputs()
click_function = "layout",
tooltip = "Left-Click: Update!\nRight-Click: Hide Tokens!",
position = { 0.725, 0.1, 2.025 },
scale = { 0.5, 0.5, 0.5 },
color = { 1, 1, 1 },
width = 675,
height = 175
font_size = 200,
width = 1350,
height = 325
})
end
@ -238,14 +242,14 @@ function deleteCopiedTokens()
end
-- creates buttons as labels as display for percentage values
function createPercentageButton(tokenCount, valueCount, tokenName)
local startPos = Vector(2.3, -0.04, 0.875 * valueCount)
function createPercentageButton(tokenCount, rowCount, tokenName)
local startPos = Vector(2.3, -0.04, 0.875 * rowCount)
if percentage == "cumulative" then
percentageLabel.scale = { 1.5, 1.5, 1.5 }
percentageLabel.scale = { 0.75, 0.75, 0.75 }
percentageLabel.position = startPos - Vector(0, 0, 2.85)
else
percentageLabel.scale = { 2, 2, 2 }
percentageLabel.scale = { 1, 1, 1 }
percentageLabel.position = startPos - Vector(0, 0, 2.675)
end
@ -254,8 +258,8 @@ function createPercentageButton(tokenCount, valueCount, tokenName)
percentageLabel.font_color = { 0.35, 0.71, 0.85 }
elseif tokenName == "Auto-fail" then
percentageLabel.font_color = { 0.86, 0.1, 0.1 }
-- check if the tokenName contains letters (e.g. symbol token)
elseif string.match(tokenName, "%a") ~= nil then
-- tokenName contains letters (e.g. symbol token)
percentageLabel.font_color = { 0.68, 0.53, 0.86 }
else
percentageLabel.font_color = { 0.85, 0.67, 0.33 }
@ -302,12 +306,14 @@ function layout(_, _, isRightClick)
end
-- clone tokens from chaos bag (default position above trash can)
local rawData = chaosBag.getData().ContainedObjects
local rawData = chaosBag.getData().ContainedObjects or {}
-- optionally get the data for tokens in play
if includeDrawnTokens then
for _, token in pairs(chaosBagApi.getTokensInPlay()) do
if token ~= nil then table.insert(rawData, token.getData()) end
if token ~= nil then
table.insert(rawData, token.getData())
end
end
end
@ -335,8 +341,15 @@ function layout(_, _, isRightClick)
end
end
-- sort table by value (symbols last if same value)
table.sort(data, tokenValueComparator)
-- handling for near empty chaos bag
if #data == 0 then
-- small delay to limit update calls
Wait.time(function() updating = false end, 0.1)
return
elseif #data > 1 then
-- sort table by value (symbols last if same value)
table.sort(data, tokenValueComparator)
end
-- laying out the tokens
local pos = self.getPosition() + Vector(3.55, -0.05, -3.95)
@ -346,21 +359,21 @@ function layout(_, _, isRightClick)
local rotation = self.getRotation()
local currentValue = data[1].value
local tokenCount = { row = 0, sum = 0, total = #data }
local valueCount = 1
local tokenName = false
local rowCount = 1
local tokenName
for i, item in ipairs(data) do
for _, item in ipairs(data) do
-- this is true for the first token in a new row
if item.value ~= currentValue then
if percentage then
tokenCount.sum = tokenCount.sum + tokenCount.row
createPercentageButton(tokenCount, valueCount, tokenName)
createPercentageButton(tokenCount, rowCount, tokenName)
end
location.x = location.x - 1.75
location.z = pos.z
currentValue = item.value
valueCount = valueCount + 1
rowCount = rowCount + 1
tokenCount.row = 0
end
@ -377,21 +390,17 @@ function layout(_, _, isRightClick)
-- this is repeated to create the button for the last token
if percentage then
tokenCount.sum = tokenCount.sum + tokenCount.row
createPercentageButton(tokenCount, valueCount, tokenName)
createPercentageButton(tokenCount, rowCount, tokenName)
end
-- introducing a small delay to limit update calls
-- small delay to limit update calls
Wait.time(function() updating = false end, 0.1)
end
-- called from outside to set default values for tokens
function onTokenDataChanged(parameters)
local tokenData = parameters.tokenData or {}
local currentScenario = parameters.currentScenario or ""
local useFrontData = parameters.useFrontData
-- update token precedence
for key, table in pairs(tokenData) do
for key, table in pairs(parameters.tokenData or {}) do
local modifier = table.modifier
if modifier == -999 then modifier = 0 end
tokenPrecedence[key][1] = modifier

View File

@ -13,7 +13,7 @@ do
---@class Request
local Request = {}
-- Sets up the ArkhamDb interface. Should be called from the parent object on load.
-- Sets up the ArkhamDb interface. Should be called from the parent object on load.
ArkhamDb.initialize = function()
configuration = internal.getConfiguration()
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
@ -71,13 +71,14 @@ do
local deck = Request.start(deckUri, function(status)
if string.find(status.text, "<!DOCTYPE html>") then
internal.maybePrint("Private deck ID " .. deckId .. " is not shared", playerColor)
internal.maybePrint("Private deck ID " .. deckId .. " is not shared.", playerColor)
return false, "Private deck " .. deckId .. " is not shared"
end
local json = JSON.decode(status.text)
local json = JSON.decode(internal.fixUtf16String(status.text))
if not json then
internal.maybePrint("Deck ID " .. deckId .. " not found", playerColor)
internal.maybePrint("Deck ID " .. deckId .. " not found.", playerColor)
return false, "Deck not found!"
end
@ -177,8 +178,8 @@ do
local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0
slots[RANDOM_WEAKNESS_ID] = nil
if randomWeaknessAmount ~= 0 then
for i=1, randomWeaknessAmount do
if randomWeaknessAmount > 0 then
for i = 1, randomWeaknessAmount do
local weaknessId = allCardsBagApi.getRandomWeaknessId()
slots[weaknessId] = (slots[weaknessId] or 0) + 1
end

View File

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

View File

@ -135,11 +135,17 @@ function discardObject(playerColor, hoveredObject)
return
end
-- warning for locations since these are usually not meant to be discarded
if hoveredObject.hasTag("Location") then
broadcastToAll("Watch out: A location was discarded.", "Yellow")
-- These should probably not be discarded normally. Ask player for confirmation.
if (hoveredObject.type == "Deck") or hoveredObject.hasTag("Location") then
local suspect = (hoveredObject.type == "Deck") and "Deck" or "Location"
Player[playerColor].showConfirmDialog("Discard " .. suspect .. "?", function () performDiscard(playerColor, hoveredObject) end)
return
end
performDiscard(playerColor, hoveredObject)
end
function performDiscard(playerColor, hoveredObject)
-- initialize list of objects to discard
local discardTheseObjects = { hoveredObject }
@ -168,7 +174,7 @@ function discardTopDeck(playerColor, hoveredObject)
else
takenCard = hoveredObject
end
Wait.frames(function() discardObject(playerColor, takenCard) end, 1)
Wait.frames(function() performDiscard(playerColor, takenCard) end, 1)
end
-- helper function to get the player to trigger the discard function for

View File

@ -222,6 +222,11 @@ function onObjectNumberTyped(hoveredObject, playerColor, number)
end
end
-- TTS event, used to redraw the playmat slot symbols after a small delay to account for the custom font loading
function onPlayerConnect()
Wait.time(function() playmatApi.redrawSlotSymbols("All") end, 0.2)
end
---------------------------------------------------------
-- chaos token drawing
---------------------------------------------------------

View File

@ -1,10 +1,13 @@
local cardIdIndex = { }
local classAndLevelIndex = { }
local basicWeaknessList = { }
local uniqueWeaknessList = { }
local cycleIndex = { }
local guidReferenceApi = require("core/GUIDReferenceApi")
local cardIdIndex = {}
local classAndLevelIndex = {}
local basicWeaknessList = {}
local uniqueWeaknessList = {}
local cycleIndex = {}
local indexingDone = false
local otherCardsDetected = false
function onLoad()
self.addContextMenuItem("Rebuild Index", startIndexBuild)
@ -12,13 +15,13 @@ function onLoad()
Wait.frames(startIndexBuild, 30)
end
-- Called by Hotfix bags when they load. If we are still loading indexes, then
-- Called by Hotfix bags when they load. If we are still loading indexes, then
-- the all cards and hotfix bags are being loaded together, and we can ignore
-- this call as the hotfix will be included in the initial indexing. If it is
-- this call as the hotfix will be included in the initial indexing. If it is
-- 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.
function rebuildIndexForHotfix()
if (indexingDone) then
if indexingDone then
startIndexBuild()
end
end
@ -26,23 +29,23 @@ end
-- Resets all current bag indexes
function clearIndexes()
indexingDone = false
cardIdIndex = { }
classAndLevelIndex = { }
classAndLevelIndex["Guardian-upgrade"] = { }
classAndLevelIndex["Seeker-upgrade"] = { }
classAndLevelIndex["Mystic-upgrade"] = { }
classAndLevelIndex["Survivor-upgrade"] = { }
classAndLevelIndex["Rogue-upgrade"] = { }
classAndLevelIndex["Neutral-upgrade"] = { }
classAndLevelIndex["Guardian-level0"] = { }
classAndLevelIndex["Seeker-level0"] = { }
classAndLevelIndex["Mystic-level0"] = { }
classAndLevelIndex["Survivor-level0"] = { }
classAndLevelIndex["Rogue-level0"] = { }
classAndLevelIndex["Neutral-level0"] = { }
cycleIndex = { }
basicWeaknessList = { }
uniqueWeaknessList = { }
cardIdIndex = {}
classAndLevelIndex = {}
classAndLevelIndex["Guardian-upgrade"] = {}
classAndLevelIndex["Seeker-upgrade"] = {}
classAndLevelIndex["Mystic-upgrade"] = {}
classAndLevelIndex["Survivor-upgrade"] = {}
classAndLevelIndex["Rogue-upgrade"] = {}
classAndLevelIndex["Neutral-upgrade"] = {}
classAndLevelIndex["Guardian-level0"] = {}
classAndLevelIndex["Seeker-level0"] = {}
classAndLevelIndex["Mystic-level0"] = {}
classAndLevelIndex["Survivor-level0"] = {}
classAndLevelIndex["Rogue-level0"] = {}
classAndLevelIndex["Neutral-level0"] = {}
cycleIndex = {}
basicWeaknessList = {}
uniqueWeaknessList = {}
end
-- Clears the bag indexes and starts the coroutine to rebuild the indexes
@ -63,6 +66,7 @@ end
function buildIndex()
local cardCount = 0
indexingDone = false
otherCardsDetected = false
-- process the allcardsbag itself
for _, cardData in ipairs(self.getData().ContainedObjects) do
@ -84,8 +88,8 @@ function buildIndex()
end
for _, cardData in ipairs(hotfixData.ContainedObjects) do
-- process containers
if cardData.ContainedObjects then
-- process containers
for _, deepCardData in ipairs(cardData.ContainedObjects) do
addCardToIndex(deepCardData)
cardCount = cardCount + 1
@ -94,8 +98,8 @@ function buildIndex()
coroutine.yield(0)
end
end
-- process single cards
else
-- process single cards
addCardToIndex(cardData)
cardCount = cardCount + 1
if cardCount > 19 then
@ -108,6 +112,7 @@ function buildIndex()
end
buildSupplementalIndexes()
updatePlayerCardPanel()
indexingDone = true
return 1
end
@ -116,8 +121,19 @@ end
---@param cardData table TTS object data for the card
function addCardToIndex(cardData)
-- using the more efficient 'json.parse()' to speed this process up
local cardMetadata = json.parse(cardData.GMNotes)
if not cardMetadata then return end
local status, cardMetadata = pcall(function() return json.parse(cardData.GMNotes) end)
-- if an error happens, fallback to the regular parser
if status ~= true or cardMetadata == nil then
log("Fast parser failed for " .. cardData.Nickname .. ", using old parser instead.")
cardMetadata = JSON.decode(cardData.GMNotes)
end
-- if metadata was not valid JSON or empty, don't add the card
if not cardMetadata == nil then
log("Error parsing " .. cardData.Nickname)
return
end
-- use the ZoopGuid as fallback if no id present
cardMetadata.id = cardMetadata.id or cardMetadata.TtsZoopGuid
@ -131,37 +147,35 @@ function addCardToIndex(cardData)
end
end
-- Creates the supplemental indexes for classes, weaknesses etc.
function buildSupplementalIndexes()
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 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
if cardMetadata.weakness then
table.insert(uniqueWeaknessList, cardMetadata.id)
if cardMetadata.basicWeaknessCount ~= nil then
for i = 1, cardMetadata.basicWeaknessCount do
table.insert(basicWeaknessList, cardMetadata.id)
if card.metadata.weakness then
table.insert(uniqueWeaknessList, card.metadata.id)
if card.metadata.basicWeaknessCount ~= nil then
for i = 1, card.metadata.basicWeaknessCount do
table.insert(basicWeaknessList, card.metadata.id)
end
end
end
-- Excludes signature cards (which have no class or level)
if cardMetadata.class ~= nil and cardMetadata.level ~= nil then
local upgradeKey
if cardMetadata.level > 0 then
if card.metadata.class ~= nil and card.metadata.level ~= nil then
local upgradeKey = "-level0"
if card.metadata.level > 0 then
upgradeKey = "-upgrade"
else
upgradeKey = "-level0"
end
-- parse classes (separated by "|") and add the card to the appropriate class and level indices
for str in cardMetadata.class:gmatch("([^|]+)") do
table.insert(classAndLevelIndex[str .. upgradeKey], cardMetadata.id)
for str in card.metadata.class:gmatch("([^|]+)") do
table.insert(classAndLevelIndex[str .. upgradeKey], card.metadata.id)
end
-- add to cycle index
local cycleName = cardMetadata.cycle
local cycleName = card.metadata.cycle
if cycleName ~= nil then
cycleName = string.lower(cycleName)
@ -170,12 +184,17 @@ function buildSupplementalIndexes()
-- override cycle name for night of the zealot
cycleName = cycleName:gsub("the night of the zealot", "core")
if cycleIndex[cycleName] == nil then
cycleIndex[cycleName] = { }
end
table.insert(cycleIndex[cycleName], cardMetadata.id)
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
cycleIndex[cycleName] = {}
end
table.insert(cycleIndex[cycleName], card.metadata.id)
end
end
end
@ -209,109 +228,173 @@ function cardComparator(id1, id2)
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()
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
end
-- Returns a specific card from the bag, based on ArkhamDB ID
-- Params table:
-- id: String ID of the card to retrieve
-- Return: If the indexes are still being constructed, an empty table is
-- returned. Otherwise, a single table with the following fields
-- cardData: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata
---@param params table ID of the card to retrieve
---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a single table with the following fields
-- data: TTS object data, suitable for spawning the card
-- metadata: Table of parsed metadata
function getCardById(params)
if (not indexingDone) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return { }
end
if not isIndexReady() then return {} end
return cardIdIndex[params.id]
end
-- Returns a list of cards from the bag matching a class and level (0 or upgraded)
-- Params table:
-- class: String class to retrieve ("Guardian", "Seeker", etc)
-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0
-- Return: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a list of tables, each with the following fields
-- cardData: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata
---@param params table
-- class: String class to retrieve ("Guardian", "Seeker", etc)
-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0
---@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 getCardsByClassAndLevel(params)
if (not indexingDone) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return { }
end
local upgradeKey
if (params.upgraded) then
if not isIndexReady() then return {} end
local upgradeKey = "-level0"
if params.upgraded then
upgradeKey = "-upgrade"
end
return classAndLevelIndex[params.class .. upgradeKey]
end
-- Returns a list of cards from the bag matching a cycle
---@param params table
-- cycle: String cycle to retrieve ("The Scarlet Keys" etc.)
-- 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
-- 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
upgradeKey = "-level0"
return card1.data.Description < card2.data.Description
end
return classAndLevelIndex[params.class..upgradeKey];
end
function getCardsByCycle(cycleName)
if (not indexingDone) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return { }
-- 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 cycleIndex[string.lower(cycleName)]
return classValue
end
-- 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.
-- 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.
-- Parameter array must contain these fields to define the search:
-- name String or string fragment to search for names
-- exact Whether the name match should be exact
-- name: String or string fragment to search for names
-- exact: Whether the name match should be exact
function getCardsByName(params)
local name = params.name
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
local addedCards = { }
local addedCards = {}
for _, cardData in pairs(cardIdIndex) do
if (not addedCards[cardData.metadata.id]) then
if (exact and (string.lower(cardData.data.Nickname) == string.lower(name)))
or (not exact and string.find(string.lower(cardData.data.Nickname), string.lower(name), 1, true)) then
table.insert(results, cardData)
addedCards[cardData.metadata.id] = true
table.insert(results, cardData)
addedCards[cardData.metadata.id] = true
end
end
end
return results
end
-- Gets a random basic weakness from the bag. Once a given ID has been returned
-- it will be removed from the list and cannot be selected again until a reload
-- occurs or the indexes are rebuilt, which will refresh the list to include all
-- weaknesses.
-- Return: String ID of the selected weakness.
-- Gets a random basic weakness from the bag. Once a given ID has been returned it will be
-- removed from the list and cannot be selected again until a reload occurs or the indexes
-- are rebuilt, which will refresh the list to include all weaknesses.
---@return string: ID of the selected weakness
function getRandomWeaknessId()
local availableWeaknesses = buildAvailableWeaknesses()
if (#availableWeaknesses > 0) then
local availableWeaknesses = buildAvailableWeaknesses()
if #availableWeaknesses > 0 then
return availableWeaknesses[math.random(#availableWeaknesses)]
end
end
-- 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
-- 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()
local weaknessesInPlay = { }
local weaknessesInPlay = {}
local allObjects = getAllObjects()
for _, object in ipairs(allObjects) do
if (object.name == "Deck") then
if object.type == "Deck" then
for _, cardData in ipairs(object.getData().ContainedObjects) do
local cardMetadata = JSON.decode(cardData.GMNotes)
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
incrementWeaknessCount(weaknessesInPlay, JSON.decode(cardData.GMNotes))
end
elseif (object.name == "Card") then
local cardMetadata = JSON.decode(object.getGMNotes())
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
elseif object.type == "Card" then
incrementWeaknessCount(weaknessesInPlay, JSON.decode(object.getGMNotes()))
end
end
local availableWeaknesses = { }
local availableWeaknesses = {}
for _, weaknessId in ipairs(basicWeaknessList) do
if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] > 0) then
weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1
@ -332,8 +415,8 @@ end
-- Helper function that adds one to the table entry for the number of weaknesses in play
function incrementWeaknessCount(table, cardMetadata)
if (isBasicWeakness(cardMetadata)) then
if (table[cardMetadata.id] == nil) then
if isBasicWeakness(cardMetadata) then
if table[cardMetadata.id] == nil then
table[cardMetadata.id] = 1
else
table[cardMetadata.id] = table[cardMetadata.id] + 1
@ -343,7 +426,7 @@ end
function isBasicWeakness(cardMetadata)
return cardMetadata ~= nil
and cardMetadata.weakness
and cardMetadata.basicWeaknessCount ~= nil
and cardMetadata.basicWeaknessCount > 0
and cardMetadata.weakness
and cardMetadata.basicWeaknessCount ~= nil
and cardMetadata.basicWeaknessCount > 0
end

View File

@ -6,22 +6,29 @@ do
return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag")
end
-- Returns a specific card from the bag, based on ArkhamDB ID
---@param id table String ID of the card to retrieve
---@return table table
-- If the indexes are still being constructed, an empty table is
-- returned. Otherwise, a single table with the following fields
-- cardData: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata
AllCardsBagApi.getCardById = function(id)
return getAllCardsBag().call("getCardById", {id = id})
-- 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
-- Gets a random basic weakness from the bag. Once a given ID has been returned
-- it will be removed from the list and cannot be selected again until a reload
-- occurs or the indexes are rebuilt, which will refresh the list to include all
-- weaknesses.
---@return string: ID of the selected weakness.
-- Returns a specific card from the bag, based on ArkhamDB ID
---@param id string ID of the card to retrieve
---@return table: If the indexes are still being constructed, returns an empty table.
-- Otherwise, a single table with the following fields
-- data: TTS object data, suitable for spawning the card
-- metadata: Table of parsed metadata
AllCardsBagApi.getCardById = function(id)
return getAllCardsBag().call("getCardById", { id = id })
end
-- Gets a random basic weakness from the bag. Once a given ID has been returned it
-- will be removed from the list and cannot be selected again until a reload occurs
-- or the indexes are rebuilt, which will refresh the list to include all weaknesses.
---@return string: ID of the selected weakness
AllCardsBagApi.getRandomWeaknessId = function()
return getAllCardsBag().call("getRandomWeaknessId")
end
@ -30,21 +37,21 @@ do
return getAllCardsBag().call("isIndexReady")
end
-- Called by Hotfix bags when they load. If we are still loading indexes, then
-- Called by Hotfix bags when they load. If we are still loading indexes, then
-- the all cards and hotfix bags are being loaded together, and we can ignore
-- this call as the hotfix will be included in the initial indexing. If it is
-- this call as the hotfix will be included in the initial indexing. If it is
-- 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.
AllCardsBagApi.rebuildIndexForHotfix = function()
return getAllCardsBag().call("rebuildIndexForHotfix")
getAllCardsBag().call("rebuildIndexForHotfix")
end
-- 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.
-- 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.
---@param name string or string fragment to search for names
---@param exact boolean Whether the name match should be exact
AllCardsBagApi.getCardsByName = function(name, exact)
return getAllCardsBag().call("getCardsByName", {name = name, exact = exact})
return returnCopyOfList(getAllCardsBag().call("getCardsByName", { name = name, exact = exact }))
end
AllCardsBagApi.isBagPresent = function()
@ -53,22 +60,29 @@ do
-- 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 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.
-- Otherwise, a list of tables, each with the following fields
-- cardData: TTS object data, suitable for spawning the card
-- cardMetadata: Table of parsed metadata
-- 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.getCardsByClassAndLevel = function(class, upgraded)
return getAllCardsBag().call("getCardsByClassAndLevel", {class = class, upgraded = upgraded})
return returnCopyOfList(getAllCardsBag().call("getCardsByClassAndLevel", { class = class, upgraded = upgraded }))
end
AllCardsBagApi.getCardsByCycle = function(cycle)
return getAllCardsBag().call("getCardsByCycle", cycle)
-- Returns a list of cards from the bag matching a 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
AllCardsBagApi.getUniqueWeaknesses = function()
return getAllCardsBag().call("getUniqueWeaknesses")
return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses"))
end
return AllCardsBagApi
end
end

View File

@ -26,22 +26,20 @@ 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}
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
-- 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
-- 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
-- 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),
@ -50,7 +48,7 @@ local START_POSITIONS = {
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.
-- should be placed. If more customizable cards are added it will need to be moved.
summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7)
}
@ -64,12 +62,12 @@ 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
-- 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
-- USE THESE! Positions and offset shifts accounting for the scale of the panel
local startPositions
local cardRowOffset
local cardGroupOffset
@ -78,7 +76,14 @@ local investigatorPositionShiftCol
local investigatorCardOffset
local investigatorSignatureOffset
local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" }
local CLASS_LIST = {
"Guardian",
"Seeker",
"Rogue",
"Mystic",
"Survivor",
"Neutral"
}
local CYCLE_LIST = {
"Core",
"The Dunwich Legacy",
@ -94,9 +99,8 @@ local CYCLE_LIST = {
}
local excludedNonBasicWeaknesses
local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
local helpVisibleToPlayers = { }
local spawnStarterDecks = false
local helpVisibleToPlayers = {}
function onSave()
return JSON.encode({ spawnBagState = spawnBag.getStateForSave() })
@ -104,7 +108,7 @@ end
function onLoad(savedData)
if savedData and savedData ~= "" then
local saveState = JSON.decode(savedData) or { }
local saveState = JSON.decode(savedData) or {}
if saveState.spawnBagState ~= nil then
spawnBag.loadFromSave(saveState.spawnBagState)
end
@ -117,7 +121,7 @@ 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 = { }
excludedNonBasicWeaknesses = {}
for _, investigator in pairs(INVESTIGATORS) do
for _, signatureId in ipairs(investigator.signatures) do
excludedNonBasicWeaknesses[signatureId] = true
@ -274,9 +278,8 @@ function createCycleButtons()
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
]]
-- 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
@ -297,8 +300,6 @@ function createClearButton()
end
function createInvestigatorModeButtons()
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
self.createButton({
function_owner = self,
click_function = "setCardsOnlyMode",
@ -306,18 +307,18 @@ function createInvestigatorModeButtons()
height = 170,
width = 760,
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({
function_owner = self,
click_function = "setStarterDeckMode",
click_function = "setspawnStarterDecks",
position = Vector(0.66, 0.1, -0.322),
height = 170,
width = 760,
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({
function_owner = self,
label = "✓",
@ -325,12 +326,77 @@ function createInvestigatorModeButtons()
position = Vector(checkX, 0.11, -0.317),
height = 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 },
color = { 1, 1, 1 }
})
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, _)
if helpVisibleToPlayers[playerColor] then
helpVisibleToPlayers[playerColor] = nil
@ -354,13 +420,13 @@ function updateHelpVisibility()
self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0)
end
function setStarterDeckMode()
starterDeckMode = STARTER_DECK_MODE_STARTERS
function setspawnStarterDecks()
spawnStarterDecks = true
updateStarterModeButtons()
end
function setCardsOnlyMode()
starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
spawnStarterDecks = false
updateStarterModeButtons()
end
@ -384,7 +450,7 @@ end
function scalePositions()
-- Assume scaling is consistent in X and Z dimensions
local scale = 1 / self.getScale().x
startPositions = { }
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
@ -405,14 +471,12 @@ function deleteAll()
spawnBag.recall(true)
end
-- Spawn an investigator group, based on the current UI setting for either investigators or starter
-- decks.
-- 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
if spawnStarterDecks then
spawnStarters(groupName)
else
spawnInvestigators(groupName)
@ -420,12 +484,12 @@ function spawnInvestigatorGroup(groupName)
end, 2)
end
-- Spawn cards for all investigators in the given group. This creates piles for all defined
-- 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")
printToAll("No investigator data for " .. groupName .. " yet")
return
end
@ -434,7 +498,7 @@ function spawnInvestigators(groupName)
local investigatorCount = #INVESTIGATOR_GROUPS[groupName]
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
spawnBag.spawn(spawnSpec)
end
@ -451,14 +515,14 @@ 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)))
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))
investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,
investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,
investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2))
return rowStart
end
@ -470,23 +534,23 @@ 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
})
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
-- 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 tts__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
--- and minicard lists. Otherwise, spawn them all in a deck
function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly)
local cardPos = Vector(position):add(investigatorCardOffset)
return {
@ -533,23 +597,24 @@ function spawnStarterDeck(investigatorName, investigatorData, position)
end
local deckPos = Vector(position):add(investigatorSignatureOffset)
arkhamDb.getDecklist("None", investigatorData.starterDeck, true, false, false, function(slots)
local cardIdList = { }
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",
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.
---@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)
@ -557,18 +622,15 @@ 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.
---@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
if not allCardsBagApi.isIndexReady() then return end
local cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded)
local skillList = { }
local eventList = { }
local assetList = { }
local skillList = {}
local eventList = {}
local assetList = {}
for _, cardId in ipairs(cardIdList) do
local cardMetadata = allCardsBagApi.getCardById(cardId).metadata
if (cardMetadata.type == "Skill") then
@ -617,21 +679,20 @@ 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)
if not allCardsBagApi.isIndexReady() then return end
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
-- sort custom cards
local sortByMetadata = false
if cycle == "Other" then
sortByMetadata = true
end
spawnBag.spawn({
name = "cycle"..cycle,
cards = copiedList,
name = "cycle" .. cycle,
cards = allCardsBagApi.getCardsByCycle(cycle, sortByMetadata),
globalPos = self.positionToWorld(startPositions.cycle),
rotation = FACE_UP_ROTATION,
spread = true,
@ -671,16 +732,13 @@ end
-- Clears the current cards, and places all basic weaknesses on the table.
function spawnWeaknesses()
if not allCardsBagApi.isIndexReady() then return end
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 basicWeaknessList = {}
local otherWeaknessList = {}
for _, id in ipairs(allCardsBagApi.getUniqueWeaknesses()) do
local cardMetadata = allCardsBagApi.getCardById(id).metadata
if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then
table.insert(basicWeaknessList, id)
@ -721,7 +779,7 @@ function spawnRandomWeakness()
prepareToPlaceCards()
local weaknessId = allCardsBagApi.getRandomWeaknessId()
if (weaknessId == nil) then
broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2})
broadcastToAll("All basic weaknesses are in play!", { 0.9, 0.2, 0.2 })
return
end
spawnBag.spawn({

View File

@ -16,7 +16,7 @@ Spawner = { }
---@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
if sort then
table.sort(cardList, Spawner.cardComparator)
end
@ -25,9 +25,9 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
local investigatorCards = { }
for _, card in ipairs(cardList) do
if (card.metadata.type == "Investigator") then
if card.metadata.type == "Investigator" then
table.insert(investigatorCards, card)
elseif (card.metadata.type == "Minicard") then
elseif card.metadata.type == "Minicard" then
table.insert(miniCards, card)
else
table.insert(standardCards, card)
@ -46,7 +46,7 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
end
Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)
if (sort) then
if sort then
table.sort(cardList, Spawner.cardComparator)
end
@ -201,7 +201,7 @@ end
---@return string id >= startId
Spawner.findNextAvailableId = function(objectTable, startId)
local id = startId
while (objectTable[id] ~= nil) do
while objectTable[id] ~= nil do
id = tostring(tonumber(id) + 1)
end
return id

View File

@ -1,32 +1,32 @@
require("playercards/PlayerCardSpawner")
-- Allows spawning of defined lists of cards which will be created from the template in the All
-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while
-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the
-- spawned objects. Objects moved out of this area will not be cleaned up.
-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while
-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the
-- spawned objects. Objects moved out of this area will not be cleaned up.
--
-- SpawnSpec: Spawning requires a spawn specification with the following structure:
-- {
-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned,
-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned,
-- but each requires a separate name
-- cards: A list of card IDs to be spawned
-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be
-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be
-- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 }
-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with
-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with
-- globalPos, this should be a valid Vector with x, y, and z defined
-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a
-- spread moving to the right. globalPos will define the location of the first card, each
-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a
-- spread moving to the right. globalPos will define the location of the first card, each
-- after that will be moved a predefined distance
-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be
-- laid out in before starting a new row. If spread is true but spreadCols is not set, all
-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be
-- laid out in before starting a new row. If spread is true but spreadCols is not set, all
-- cards will be in a single row (however long that may be)
-- }
-- See BondedBag.ttslua for an example
do
local allCardsBagApi = require("playercards/AllCardsBagApi")
local SpawnBag = { }
local internal = { }
local SpawnBag = {}
local internal = {}
-- To assist debugging, will draw a box around the recall zone when it's set up
local SHOW_RECALL_ZONE = false
@ -36,8 +36,8 @@ do
local RECALL_BUFFER_Z = 0.5
-- In order to mimic the behavior of the previous memory buttons we use a temporary bag when
-- recalling objects. This bag is tiny and transparent, and will be placed at the same location as
-- this object. Once all placed cards are recalled bag to this bag, it will be destroyed
-- recalling objects. This bag is tiny and transparent, and will be placed at the same location as
-- this object. Once all placed cards are recalled bag to this bag, it will be destroyed
local RECALL_BAG = {
Name = "Bag",
Transform = {
@ -58,8 +58,8 @@ do
}
-- Tracks what has been placed by this "bag" so they can be recalled
local placedSpecs = { }
local placedObjectGuids = { }
local placedSpecs = {}
local placedObjectGuids = {}
local recallZone = nil
-- Loads a table of saved state, extracted during the parent object's onLoad
@ -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
SpawnBag.spawn = function(spawnSpec)
-- Limit to one placement at a time
if (placedSpecs[spawnSpec.name]) then
return
end
if (spawnSpec == nil) then
-- TODO: error here
return
end
local cardsToSpawn = { }
local cardList = spawnSpec.cards
for _, cardId in ipairs(cardList) do
local cardData = allCardsBagApi.getCardById(cardId)
if (cardData ~= nil) then
table.insert(cardsToSpawn, cardData)
else
-- TODO: error here
if placedSpecs[spawnSpec.name] or spawnSpec == nil then return end
local cardsToSpawn = {}
for _, cardId in ipairs(spawnSpec.cards) do
local card = allCardsBagApi.getCardById(cardId)
if card ~= nil then
table.insert(cardsToSpawn, card)
end
end
if (spawnSpec.spread) then
if spawnSpec.spread then
Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject)
else
-- 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()
end
-- We've recalled everything we can, some cards may have been moved out of the
-- card area. Just reset at this point.
placedSpecs = { }
placedObjectGuids = { }
-- We've recalled everything we can, some cards may have been moved out of the card area. Just reset at this point.
placedSpecs = {}
placedObjectGuids = {}
recallZone = nil
end
-- Deleted all spawned cards.
-- Delete all spawned cards
internal.deleteSpawned = function()
for guid, _ in pairs(placedObjectGuids) do
local obj = getObjectFromGUID(guid)
@ -140,9 +131,9 @@ do
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()
local trash = spawnObjectData({data = RECALL_BAG, position = self.getPosition()})
local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() })
for guid, _ in pairs(placedObjectGuids) do
local obj = getObjectFromGUID(guid)
if (obj ~= nil) then
@ -156,71 +147,70 @@ do
trash.destruct()
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)
placedObjectGuids[spawned.getGUID()] = true
internal.expandRecallZone(spawned)
end
-- Expands the current recall zone based on the position of the given object. The recall zone will
-- Expands the current recall zone based on the position of the given object. The recall zone will
-- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer
internal.expandRecallZone = function(spawnedCard)
local pos = spawnedCard.getPosition()
if (recallZone == nil) then
-- First card out of the bag, initialize surrounding that
recallZone = { }
recallZone = {}
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 }
return
else
if (pos.x > recallZone.upperLeft.x) then
recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X
end
if (pos.x < recallZone.lowerRight.x) then
recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X
end
if (pos.z > recallZone.upperLeft.z) then
recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z
end
if (pos.z < recallZone.lowerRight.z) then
recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z
end
end
if (SHOW_RECALL_ZONE) then
if pos.x > recallZone.upperLeft.x then
recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X
end
if pos.x < recallZone.lowerRight.x then
recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X
end
if pos.z > recallZone.upperLeft.z then
recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z
end
if pos.z < recallZone.lowerRight.z then
recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z
end
if SHOW_RECALL_ZONE then
local y = 1.5
local thick = 0.05
Global.setVectorLines({
{
points = { {recallZone.upperLeft.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.lowerRight.z} },
color = {1,0,0},
points = { { recallZone.upperLeft.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.lowerRight.z } },
color = { 1, 0, 0 },
thickness = thick,
rotation = {0,0,0}
rotation = { 0, 0, 0 }
},
{
points = { {recallZone.upperLeft.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.lowerRight.z} },
color = {1,0,0},
points = { { recallZone.upperLeft.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.lowerRight.z } },
color = { 1, 0, 0 },
thickness = thick,
rotation = {0,0,0}
rotation = { 0, 0, 0 }
},
{
points = { {recallZone.lowerRight.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.upperLeft.z} },
color = {1,0,0},
points = { { recallZone.lowerRight.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.upperLeft.z } },
color = { 1, 0, 0 },
thickness = thick,
rotation = {0,0,0}
rotation = { 0, 0, 0 }
},
{
points = { {recallZone.lowerRight.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.upperLeft.z} },
color = {1,0,0},
points = { { recallZone.lowerRight.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.upperLeft.z } },
color = { 1, 0, 0 },
thickness = thick,
rotation = {0,0,0}
rotation = { 0, 0, 0 }
}
})
end
end
-- Checks to see if the given object is in the current recall zone. If there isn't a recall zone,
-- Checks to see if the given object is in the current recall zone. If there isn't a recall zone,
-- will return true so that everything can be easily cleaned up.
internal.isInRecallZone = function(obj)
if (recallZone == nil) then
@ -228,11 +218,11 @@ do
end
local pos = obj.getPosition()
return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x
and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z)
and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z)
end
internal.reverseList = function(list)
local reversed = { }
local reversed = {}
for i = 1, #list do
reversed[i] = list[#list - i + 1]
end

View File

@ -73,8 +73,8 @@ local slotNameToChar = {
["Tarot"] = "A"
}
-- slot symbol for the respective slot (from top left to bottom right)
local slotData = {}
-- slot symbol for the respective slot (from top left to bottom right) - intentionally global!
slotData = {}
local defaultSlotData = {
-- 1st row
"any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally",
@ -303,7 +303,7 @@ function doUpkeep(_, clickedByColor, isRightClick)
obj.flip()
elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
if not (obj.getVar("do_not_ready") or false) then
if not (obj.getVar("do_not_ready") or obj.hasTag("DoNotReady")) then
local cardRotation = round(obj.getRotation().y, 0) - rot.y
local yRotDiff = 0

View File

@ -55,6 +55,25 @@ do
end
end
-- gets the slot data for the playmat
---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All")
PlaymatApi.getSlotData = function(matColor)
for _, mat in pairs(getMatForColor(matColor)) do
return mat.getTable("slotData")
end
end
-- sets the slot data for the playmat
---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All")
---@param newSlotData table New slot data for the playmat
PlaymatApi.loadSlotData = function(matColor, newSlotData)
for _, mat in pairs(getMatForColor(matColor)) do
mat.setTable("slotData", newSlotData)
mat.call("redrawSlotSymbols")
return
end
end
-- Performs a search of the deck area of the requested playmat and returns the result as table
---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All")
PlaymatApi.getDeckAreaObjects = function(matColor)
@ -258,6 +277,14 @@ do
end
end
-- Redraws the XML for the slot symbols based on the slotData table
---@param matColor string Color of the playmat - White, Orange, Green, Red or All
PlaymatApi.redrawSlotSymbols = function(matColor)
for _, mat in pairs(getMatForColor(matColor)) do
mat.call("redrawSlotSymbols")
end
end
-- Finds all objects on the playmat and associated set aside zone and returns a table
---@param matColor string Color of the playmat - White, Orange, Green, Red or All
---@param filter string Name of the filte function (see util/SearchLib)

View File

@ -1,87 +1,98 @@
local lines = {}
local connections = {}
-- save "lines" to be able to remove them after loading
function onSave()
return JSON.encode(lines)
return JSON.encode({ connections = connections })
end
function onLoad(savedData)
if savedData and savedData ~= "" then
lines = JSON.decode(savedData) or {}
local loadedData = JSON.decode(savedData) or {}
connections = loadedData.connections
processLines()
end
addHotkey("Drawing Tool: Reset", function() connections = {} processLines() end)
addHotkey("Drawing Tool: Redraw", processLines)
end
-- create timer when numpad 0 is pressed
function onScriptingButtonDown(index, player_color)
function onScriptingButtonDown(index, playerColor)
if index ~= 10 then return end
TimerID = Wait.time(function() draw_from(Player[player_color]) end, 1)
Timer.create {
identifier = playerColor .. "_draw_from",
function_name = "draw_from",
parameters = { player = Player[playerColor] },
delay = 1
}
end
-- called for long press of numpad 0, draws lines from hovered object to selected objects
function draw_from(player)
local source = player.getHoverObject()
function draw_from(params)
local source = params.player.getHoverObject()
if not source then return end
for _, item in ipairs(player.getSelectedObjects()) do
if item.getGUID() ~= source.getGUID() then
for _, item in ipairs(params.player.getSelectedObjects()) do
if item ~= source then
if item.getGUID() > source.getGUID() then
draw_with_pair(item, source)
addPair(item, source)
else
draw_with_pair(source, item)
addPair(source, item)
end
end
end
process_lines()
processLines()
end
-- general drawing of all lines between selected objects
function onScriptingButtonUp(index, player_color)
function onScriptingButtonUp(index, playerColor)
if index ~= 10 then return end
-- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second.
if not Wait.stop(TimerID) then return end
local items = Player[player_color].getSelectedObjects()
if #items < 2 then
broadcastToColor("You must have at least two items selected (currently: " .. #items .. ").", player_color, "Red")
return
end
-- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second.
if not Timer.destroy(playerColor .. "_draw_from") then return end
local items = Player[playerColor].getSelectedObjects()
if #items < 2 then return end
table.sort(items, function(a, b) return a.getGUID() > b.getGUID() end)
for f = 1, #items - 1 do
for s = f + 1, #items do
draw_with_pair(items[f], items[s])
for i = 1, #items do
local first = items[i]
for j = i, #items do
local second = items[j]
addPair(first, second)
end
end
process_lines()
processLines()
end
-- adds two objects to table of vector lines
function draw_with_pair(first, second)
local guid_first = first.getGUID()
local guid_second = second.getGUID()
function addPair(first, second)
local first_guid = first.getGUID()
local second_guid = second.getGUID()
if Global.getVectorLines() == nil then lines = {} end
if not lines[guid_first] then lines[guid_first] = {} end
if lines[guid_first][guid_second] then
lines[guid_first][guid_second] = nil
else
lines[guid_first][guid_second] = { points = { first.getPosition(), second.getPosition() }, color = "White" }
end
if not connections[first_guid] then connections[first_guid] = {} end
connections[first_guid][second_guid] = not connections[first_guid][second_guid]
end
-- updates the global vector lines based on "lines"
function process_lines()
local drawing = {}
function processLines()
local lines = {}
for _, first in pairs(lines) do
for _, data in pairs(first) do
table.insert(drawing, data)
for source_guid, target_guids in pairs(connections) do
local source = getObjectFromGUID(source_guid)
for target_guid, exists in pairs(target_guids) do
if exists then
local target = getObjectFromGUID(target_guid)
if source and target then
table.insert(lines, {
points = { source.getPosition(), target.getPosition() },
color = Color.White
})
end
end
end
end
Global.setVectorLines(drawing)
Global.setVectorLines(lines)
end

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>