diff --git a/config.json b/config.json
index 2adead0d..1335be54 100644
--- a/config.json
+++ b/config.json
@@ -154,7 +154,7 @@
"ChaosBagStatTracker.766620",
"Blesstokens.afa06b",
"Cursetokens.bd0253",
- "WhimsicalsTokenRemover.0a5a29",
+ "TokenRemover.0a5a29",
"TokenSpawner.36b4ee",
"Fan-MadeScenariosCampaignsMiscellany.66e97c",
"OfficialStandaloneChallengeScenarios.0ef5c8",
@@ -250,7 +250,8 @@
"TokenSpawnTracker.e3ffc9",
"TokenSource.124381",
"GameData.3dbe47",
- "SCEDTour.0e5aa8"
+ "SCEDTour.0e5aa8",
+ "PlayerCards.2d30ee"
],
"PlayArea": 1,
"PlayerCounts": [
diff --git a/modsettings/TabStates.json b/modsettings/TabStates.json
index 7b9e9a7c..14d7babc 100644
--- a/modsettings/TabStates.json
+++ b/modsettings/TabStates.json
@@ -1,6 +1,6 @@
{
"10": {
- "body": "Created by Whimsical\n\nAnything that passes over the remover that isn't a card or a deck will be deleted.\r\nTo use the remover, right click on it, choose the \"Enable\" option, and take your card with resources/horror/damage and swipe it over the remover. You may wish to unlock and/or copy the remover to your play area first.",
+ "body": "Created by Whimsical\n\nAnything that passes over the remover that isn't a card, deck or chaos token will be deleted.\r\nTo use the remover, right click on it, choose the \"Enable\" option, and take your card with resources/horror/damage and swipe it over the remover. You may wish to unlock and/or copy the remover to your play area first.",
"color": "Grey",
"id": 10,
"title": "Token Remover",
diff --git a/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.json b/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.json
index ea1b770c..86a797b0 100644
--- a/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.json
+++ b/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.json
@@ -33,7 +33,7 @@
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
- "LuaScriptState": "{\"Bless\":8,\"Curse\":0}",
+ "LuaScriptState": "",
"LuaScript_path": "Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.ttslua",
"MeasureMovement": false,
"Name": "Custom_Token",
@@ -48,9 +48,9 @@
"posX": 22.215,
"posY": 5.651,
"posZ": -34.811,
- "rotX": 4,
+ "rotX": 0,
"rotY": 270,
- "rotZ": 357,
+ "rotZ": 0,
"scaleX": 2.5,
"scaleY": 1,
"scaleZ": 2.5
diff --git a/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.ttslua b/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.ttslua
index 10d54327..a64b2883 100644
--- a/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.ttslua
+++ b/objects/Fan-MadeAccessories.aa8b38/ChaosBagManager.023240.ttslua
@@ -62,7 +62,7 @@ function onLoad()
-- create buttons for tokens
for i = 1, #BUTTON_POSITION do
local funcName = "buttonClick" .. i
- self.setVar(funcName, function(_, _, isRightClick) buttonClick(_, _, isRightClick, i) end)
+ self.setVar(funcName, function(_, _, isRightClick) buttonClick(i, isRightClick) end)
buttonParameters.click_function = funcName
buttonParameters.tooltip = BUTTON_TOOLTIP[i]
@@ -70,8 +70,8 @@ function onLoad()
if i < 7 then
buttonParameters.position.z = -0.778
- elseif i > 12 then
- buttonParameters.position.z = 0.75
+ elseif i > 11 then
+ buttonParameters.position.z = 0.755
end
self.createButton(buttonParameters)
@@ -111,7 +111,7 @@ function getChaosBag()
end
-- click function for buttons
-function buttonClick(_, _, isRightClick, index)
+function buttonClick(index, isRightClick)
chaosbag = getChaosBag()
-- error handling: chaos bag not found
diff --git a/objects/Fan-MadeAccessories.aa8b38/HandHelper.450688.ttslua b/objects/Fan-MadeAccessories.aa8b38/HandHelper.450688.ttslua
index 3204c68f..d79dd093 100644
--- a/objects/Fan-MadeAccessories.aa8b38/HandHelper.450688.ttslua
+++ b/objects/Fan-MadeAccessories.aa8b38/HandHelper.450688.ttslua
@@ -153,6 +153,7 @@ end
---------------------------------------------------------
-- discards a random card from hand
---------------------------------------------------------
+
function discardRandom()
if not playerExists(playerColor) then return end
@@ -162,10 +163,8 @@ function discardRandom()
broadcastToAll("Cannot discard from empty hand!", "Red")
else
local searchPos = Player[playerColor].getHandTransform().position
- local mat = playmatAPI.getMatbyPosition(searchPos)
- if mat == nil then return end
- local discardPos = mat.getTable("DISCARD_PILE_POSITION")
+ local discardPos = playmatAPI.getDiscardPosition(playmatAPI.getMatColorByPosition(searchPos))
if discardPos == nil then
broadcastToAll("Couldn't retrieve discard position from playermat!", "Red")
return
diff --git a/objects/LuaScriptState.luascriptstate b/objects/LuaScriptState.luascriptstate
index 277d64b4..ce0cb518 100644
--- a/objects/LuaScriptState.luascriptstate
+++ b/objects/LuaScriptState.luascriptstate
@@ -1 +1 @@
-{"optionPanel":{"showChaosBagManager":false,"showCleanUpHelper":false,"showDrawButton":false,"showHandHelper":[],"showNavigationOverlay":false,"showTokenArranger":false,"useClueClickers":false,"useSnapTags":true}}
+{"optionPanel":{"showAttachmentHelper":false,"showChaosBagManager":false,"showCleanUpHelper":false,"useClueClickers":false,"showCustomPlaymatImages":false,"showCYOA":false,"showDisplacementTool":false,"showDrawButton":false,"showHandHelper":[],"showNavigationOverlay":false,"useSnapTags":true,"showTitleSplash":true,"showTokenArranger":false}}
diff --git a/objects/PlayerCards.2d30ee.json b/objects/PlayerCards.2d30ee.json
new file mode 100644
index 00000000..8bea32fe
--- /dev/null
+++ b/objects/PlayerCards.2d30ee.json
@@ -0,0 +1,57 @@
+{
+ "AltLookAngle": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "Autoraise": true,
+ "ColorDiffuse": {
+ "b": 1,
+ "g": 1,
+ "r": 1
+ },
+ "CustomImage": {
+ "CustomTile": {
+ "Stackable": false,
+ "Stretch": true,
+ "Thickness": 0.1,
+ "Type": 3
+ },
+ "ImageScalar": 1,
+ "ImageSecondaryURL": "https://i.imgur.com/dISlnEk.jpg",
+ "ImageURL": "https://i.imgur.com/dISlnEk.jpg",
+ "WidthScale": 0
+ },
+ "Description": "",
+ "DragSelectable": true,
+ "GMNotes": "",
+ "GUID": "2d30ee",
+ "Grid": true,
+ "GridProjection": false,
+ "Hands": false,
+ "HideWhenFaceDown": false,
+ "IgnoreFoW": false,
+ "LayoutGroupSortIndex": 0,
+ "Locked": false,
+ "LuaScript": "require(\"playercards/PlayerCardPanel\")",
+ "LuaScriptState": "{\"spawnBagState\":{\"placed\":[],\"placedObjects\":[]}}",
+ "MeasureMovement": false,
+ "Name": "Custom_Tile",
+ "Nickname": "Player Cards",
+ "Snap": true,
+ "Sticky": true,
+ "Tooltip": false,
+ "Transform": {
+ "posX": -1.083,
+ "posY": 1.255,
+ "posZ": 69.985,
+ "rotX": 0,
+ "rotY": 270,
+ "rotZ": 0,
+ "scaleX": 9.39,
+ "scaleY": 1,
+ "scaleZ": 9.39
+ },
+ "Value": 0,
+ "XmlUI": ""
+}
diff --git a/objects/WhimsicalsTokenRemover.0a5a29.json b/objects/TokenRemover.0a5a29.json
similarity index 86%
rename from objects/WhimsicalsTokenRemover.0a5a29.json
rename to objects/TokenRemover.0a5a29.json
index 3778f096..cc3be20c 100644
--- a/objects/WhimsicalsTokenRemover.0a5a29.json
+++ b/objects/TokenRemover.0a5a29.json
@@ -14,7 +14,7 @@
"CustomTile": {
"Stackable": false,
"Stretch": true,
- "Thickness": 0.2,
+ "Thickness": 0.1,
"Type": 0
},
"ImageScalar": 1,
@@ -34,10 +34,10 @@
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScript": "require(\"util/TokenRemover\")",
- "LuaScriptState": "[]",
+ "LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Tile",
- "Nickname": "Whimsical's Token Remover",
+ "Nickname": "Token Remover",
"Snap": true,
"Sticky": true,
"Tags": [
@@ -45,16 +45,16 @@
],
"Tooltip": true,
"Transform": {
- "posX": -6.868,
+ "posX": -7,
"posY": 1.583,
- "posZ": -16.394,
+ "posZ": -16.4,
"rotX": 0,
"rotY": 90,
"rotZ": 0,
- "scaleX": 0.75,
+ "scaleX": 0.8,
"scaleY": 1,
- "scaleZ": 0.75
+ "scaleZ": 0.8
},
"Value": 0,
"XmlUI": ""
-}
+}
\ No newline at end of file
diff --git a/objects/TokenSource.124381.json b/objects/TokenSource.124381.json
index 9b468f66..503bb56f 100644
--- a/objects/TokenSource.124381.json
+++ b/objects/TokenSource.124381.json
@@ -18,7 +18,8 @@
"Damage.cd2a02",
"Horror.36be72",
"ClueDoom.a3fb6c",
- "Resource.00d19a"
+ "Resource.00d19a",
+ "ResourceCounter.498ec0"
],
"ContainedObjects_path": "TokenSource.124381",
"Description": "",
diff --git a/objects/TokenSource.124381/ClueDoom.a3fb6c.json b/objects/TokenSource.124381/ClueDoom.a3fb6c.json
index 48b61ac0..3e32ac21 100644
--- a/objects/TokenSource.124381/ClueDoom.a3fb6c.json
+++ b/objects/TokenSource.124381/ClueDoom.a3fb6c.json
@@ -40,7 +40,7 @@
"Nickname": "ClueDoom",
"Snap": false,
"Sticky": true,
- "Tooltip": false,
+ "Tooltip": true,
"Transform": {
"posX": 78.661,
"posY": 2.398,
diff --git a/objects/TokenSource.124381/ClueDoom.a40a48.json b/objects/TokenSource.124381/ClueDoom.a40a48.json
index 9720c15b..c501cd2f 100644
--- a/objects/TokenSource.124381/ClueDoom.a40a48.json
+++ b/objects/TokenSource.124381/ClueDoom.a40a48.json
@@ -40,7 +40,7 @@
"Nickname": "ClueDoom",
"Snap": false,
"Sticky": true,
- "Tooltip": false,
+ "Tooltip": true,
"Transform": {
"posX": 78.738,
"posY": 2.287,
diff --git a/objects/TokenSource.124381/Resource.00d19a.json b/objects/TokenSource.124381/Resource.00d19a.json
index ac96f326..0d1c5508 100644
--- a/objects/TokenSource.124381/Resource.00d19a.json
+++ b/objects/TokenSource.124381/Resource.00d19a.json
@@ -40,7 +40,7 @@
"Nickname": "Resource",
"Snap": false,
"Sticky": true,
- "Tooltip": false,
+ "Tooltip": true,
"Transform": {
"posX": 78.848,
"posY": 2.273,
diff --git a/objects/TokenSource.124381/ResourceCounter.498ec0.json b/objects/TokenSource.124381/ResourceCounter.498ec0.json
new file mode 100644
index 00000000..c54a1d52
--- /dev/null
+++ b/objects/TokenSource.124381/ResourceCounter.498ec0.json
@@ -0,0 +1,57 @@
+{
+ "AltLookAngle": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "Autoraise": true,
+ "ColorDiffuse": {
+ "b": 1,
+ "g": 1,
+ "r": 1
+ },
+ "CustomImage": {
+ "CustomToken": {
+ "MergeDistancePixels": 5,
+ "Stackable": false,
+ "StandUp": false,
+ "Thickness": 0.1
+ },
+ "ImageScalar": 1,
+ "ImageSecondaryURL": "",
+ "ImageURL": "http://cloud-3.steamusercontent.com/ugc/949599153663401115/EAA6D40FC6E15204BBE551BCDED35CC8C75111BF/",
+ "WidthScale": 0
+ },
+ "Description": "0",
+ "DragSelectable": true,
+ "GMNotes": "resourceCounter",
+ "GUID": "498ec0",
+ "Grid": true,
+ "GridProjection": false,
+ "Hands": false,
+ "HideWhenFaceDown": false,
+ "IgnoreFoW": false,
+ "LayoutGroupSortIndex": 0,
+ "Locked": false,
+ "LuaScript": "require(\"core/GenericCounter\")",
+ "LuaScriptState": "0",
+ "MeasureMovement": false,
+ "Name": "Custom_Token",
+ "Nickname": "Resource Counter",
+ "Snap": false,
+ "Sticky": true,
+ "Tooltip": false,
+ "Transform": {
+ "posX": 0,
+ "posY": 3,
+ "posZ": 0,
+ "rotX": 0,
+ "rotY": 270,
+ "rotZ": 0,
+ "scaleX": 0.26,
+ "scaleY": 1,
+ "scaleZ": 0.26
+ },
+ "Value": 0,
+ "XmlUI": ""
+}
\ No newline at end of file
diff --git a/src/arkhamdb/ArkhamDb.ttslua b/src/arkhamdb/ArkhamDb.ttslua
index 63ed2ffe..97f853f0 100644
--- a/src/arkhamdb/ArkhamDb.ttslua
+++ b/src/arkhamdb/ArkhamDb.ttslua
@@ -134,15 +134,6 @@ do
internal.maybePrint(table.concat({ "Found decklist: ", deck.name }), playerColor)
- log(table.concat({ "-", deck.name, "-" }))
- for k, v in pairs(deck) do
- if type(v) == "table" then
- log(table.concat { k, ":
" })
- else
- log(table.concat { k, ": ", tostring(v) })
- end
- end
-
-- Initialize deck slot table and perform common transformations. The order of these should not
-- be changed, as later steps may act on cards added in each. For example, a random weakness or
-- investigator may have bonded cards or taboo entries, and should be present
diff --git a/src/core/GenericCounter.ttslua b/src/core/GenericCounter.ttslua
index c3f9cce8..d21ff1c7 100644
--- a/src/core/GenericCounter.ttslua
+++ b/src/core/GenericCounter.ttslua
@@ -1,50 +1,52 @@
-MIN_VALUE = -99
-MAX_VALUE = 999
+MIN_VALUE = 0
+MAX_VALUE = 99
val = 0
function onSave() return JSON.encode(val) end
-function onLoad(saved_data)
- if saved_data ~= nil then
- val = JSON.decode(saved_data)
- end
+function onLoad(savedData)
+ if savedData ~= nil then
+ val = JSON.decode(savedData)
+ end
- local name = self.getName()
- local position = {}
+ local name = self.getName()
+ local position = {}
- if name == "Damage" or name == "Resources" then
- position = { 0, 0.06, 0.1 }
- elseif name == "Horror" then
- position = { -0.025, 0.06, -0.025 }
- else
- position = { 0, 0.06, 0 }
- end
+ if name == "Damage" or name == "Resources" or name == "Resource Counter" then
+ position = { 0, 0.06, 0.1 }
+ elseif name == "Horror" then
+ position = { -0.025, 0.06, -0.025 }
+ else
+ position = { 0, 0.06, 0 }
+ end
- self.createButton({
- label = tostring(val),
- click_function = "addOrSubtract",
- function_owner = self,
- position = position,
- height = 600,
- width = 1000,
- scale = { 1.5, 1.5, 1.5 },
- font_size = 600,
- font_color = { 1, 1, 1, 100 },
- color = { 0, 0, 0, 0 }
- })
+ self.createButton({
+ label = tostring(val),
+ click_function = "addOrSubtract",
+ function_owner = self,
+ position = position,
+ height = 600,
+ width = 1000,
+ scale = { 1.5, 1.5, 1.5 },
+ font_size = 600,
+ font_color = { 1, 1, 1, 100 },
+ color = { 0, 0, 0, 0 }
+ })
+
+ self.addContextMenuItem("Add 5", function() updateVal(val + 5) end)
+ self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end)
+ self.addContextMenuItem("Add 10", function() updateVal(val + 10) end)
+ self.addContextMenuItem("Subtract 10", function() updateVal(val - 10) end)
end
function updateVal(newVal)
- if tonumber(newVal) then
- val = newVal
- self.editButton({
- index = 0,
- label = tostring(val)
- })
- end
+ if tonumber(newVal) then
+ val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)
+ self.editButton({ index = 0, label = tostring(val) })
+ end
end
-function addOrSubtract(_, _, alt_click)
- val = math.min(math.max(val + (alt_click and -1 or 1), MIN_VALUE), MAX_VALUE)
- self.editButton({ index = 0, label = tostring(val) })
+function addOrSubtract(_, _, isRightClick)
+ val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)
+ self.editButton({ index = 0, label = tostring(val) })
end
diff --git a/src/core/Global.ttslua b/src/core/Global.ttslua
index 01c0729a..93079d48 100644
--- a/src/core/Global.ttslua
+++ b/src/core/Global.ttslua
@@ -1,5 +1,3 @@
-local tokenManager = require("core/token/TokenManager")
-
---------------------------------------------------------
-- general setup
---------------------------------------------------------
@@ -34,7 +32,9 @@ local chaosTokens = {}
local chaosTokensLastMat = nil
local IS_RESHUFFLING = false
local bagSearchers = {}
+local hideTitleSplashWaitFunctionId = nil
local playmatAPI = require("playermat/PlaymatApi")
+local tokenManager = require("core/token/TokenManager")
---------------------------------------------------------
-- data for tokens
@@ -818,12 +818,20 @@ function applyOptionPanelChange(id, state)
-- update master clue counter
getObjectFromGUID("4a3aa4").setVar("useClickableCounters", state)
+ -- option: Clickable resource counters
+ elseif id == "useResourceCounters" then
+ optionPanel[id] = state
+
+ -- option: Show Title on placing scenarios
+ elseif id == "showTitleSplash" then
+ optionPanel[id] = state
+
-- option: Show token arranger
elseif id == "showTokenArranger" then
-- delete previously pulled out tokens
for _, token in ipairs(getObjectsWithTag("to_be_deleted")) do token.destruct() end
- optionPanel[id] = spawnOrRemoveHelper(state, "Token Arranger", {-42.3, 1.4, -46.5})
+ optionPanel[id] = spawnOrRemoveHelper(state, "Token Arranger", {-42.3, 1.6, -46.5})
-- option: Show clean up helper
elseif id == "showCleanUpHelper" then
@@ -838,11 +846,27 @@ function applyOptionPanelChange(id, state)
-- option: Show chaos bag manager
elseif id == "showChaosBagManager" then
- optionPanel[id] = spawnOrRemoveHelper(state, "Chaos Bag Manager", {-67.8, 1.4, -49.5})
+ optionPanel[id] = spawnOrRemoveHelper(state, "Chaos Bag Manager", {-67.8, 1.6, -49.5})
+
+ -- option: Show attachment helper
+ elseif id == "showAttachmentHelper" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Attachment Helper", {-64, 1.4, 0})
-- option: Show navigation overlay
elseif id == "showNavigationOverlay" then
- optionPanel[id] = spawnOrRemoveHelper(state, "jaqenZann's Navigation Overlay", {-11.7, 1.4, -15})
+ optionPanel[id] = spawnOrRemoveHelper(state, "jaqenZann's Navigation Overlay", {-11.7, 1.6, -15})
+
+ -- option: Show CYOA campaign guides
+ elseif id == "showCYOA" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "CYOA Campaign Guides", {21.2, 1.4, -65.7})
+
+ -- option: Show custom playmat images
+ elseif id == "showCustomPlaymatImages" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Custom Playmat Images", {12, 1.4, -46.5})
+
+ -- option: Show displacement tool
+ elseif id == "showDisplacementTool" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Displacement Tool", {-60.7, 1.4, -49.5})
end
end
@@ -851,8 +875,8 @@ end
---@param name String Name of the helper object
---@param position Vector Position of the object (where it will spawn)
---@param rotation Vector Rotation of the object for spawning (default: {0, 270, 0})
----@param color Color This is only needed for correctly setting the color of the "Hand Helper"
--- returns the GUID of the spawnedObj (or nil if object was removed)
+---@param color String This is only needed for correctly setting the color of the "Hand Helper"
+---@return. GUID of the spawnedObj (or nil if object was removed)
function spawnOrRemoveHelper(state, name, position, rotation, color)
if state then
Player.getPlayers()[1].pingTable(position)
@@ -864,7 +888,7 @@ end
-- copies the specified tool (by name) from the barrel
---@param name String Name of the object that should be copied
----@param position Position Desired position of the object
+---@param position Table Desired position of the object
function spawnHelperObject(name, position, rotation, color)
local barrel = getObjectFromGUID(BARREL_GUID)
@@ -907,7 +931,11 @@ function removeHelperObject(name)
["Clean Up Helper"] = "showCleanUpHelper",
["Hand Helper"] = "showHandHelper",
["Chaos Bag Manager"] = "showChaosBagManager",
- ["jaqenZann's Navigation Overlay"] = "showNavigationOverlay"
+ ["jaqenZann's Navigation Overlay"] = "showNavigationOverlay",
+ ["Displacement Tool"] = "showDisplacementTool",
+ ["Custom Playmat Images"] = "showCustomPlaymatImages",
+ ["Attachment Helper"] = "showAttachmentHelper",
+ ["CYOA Campaign Guides"] = "showCYOA"
}
local data = optionPanel[referenceTable[name]]
@@ -931,7 +959,7 @@ function onClick_defaultSettings()
for id, _ in pairs(optionPanel) do
local state = false
-- override for settings that are enabled by default
- if id == "useSnapTags" then
+ if id == "useSnapTags" or id == "showTitleSplash" then
state = true
end
applyOptionPanelChange(id, state)
@@ -939,16 +967,43 @@ function onClick_defaultSettings()
-- clean reset of variable
optionPanel = {
- useSnapTags = true,
- showDrawButton = false,
- useClueClickers = false,
- showTokenArranger = false,
+ showAttachmentHelper = false,
showCleanUpHelper = false,
- showHandHelper = {},
showChaosBagManager = false,
- showNavigationOverlay = false
+ showCustomPlaymatImages = false,
+ showCYOA = false,
+ showDisplacementTool = false,
+ showDrawButton = false,
+ showHandHelper = {},
+ showNavigationOverlay = false,
+ showTitleSplash = true,
+ showTokenArranger = false,
+ useClueClickers = false,
+ useSnapTags = true
}
-- update UI
updateOptionPanelState()
end
+
+-- splash scenario title on setup
+function titleSplash(scenarioName)
+ if optionPanel['showTitleSplash'] then
+
+ -- if there's any ongoing title being displayed, hide it and cancel the waiting function
+ if hideTitleSplashWaitFunctionId then
+ Wait.stop(hideTitleSplashWaitFunctionId)
+ hideTitleSplashWaitFunctionId = nil
+ UI.setAttribute('title_splash', 'active', false)
+ end
+
+ -- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)
+ -- wait timer to hide the scenario name
+ UI.setValue('title_splash', scenarioName)
+ UI.show('title_splash')
+ hideTitleSplashWaitFunctionId = Wait.time(function()
+ UI.hide('title_splash')
+ hideTitleSplashWaitFunctionId = nil
+ end, 4)
+ end
+end
diff --git a/src/core/MythosArea.ttslua b/src/core/MythosArea.ttslua
index 84f962ab..94933d64 100644
--- a/src/core/MythosArea.ttslua
+++ b/src/core/MythosArea.ttslua
@@ -55,6 +55,7 @@ function resetTokensIfInDeckZone(container, object)
end
function fireScenarioChangedEvent()
+ Global.call('titleSplash', currentScenario)
playArea.onScenarioChanged(currentScenario)
end
diff --git a/src/core/PlayArea.ttslua b/src/core/PlayArea.ttslua
index 3da4afec..ba772aa9 100644
--- a/src/core/PlayArea.ttslua
+++ b/src/core/PlayArea.ttslua
@@ -3,10 +3,24 @@
---------------------------------------------------------
-- set true to enable debug logging
-DEBUG = false
+local DEBUG = false
+
+-- Location connection directional options
+local BIDIRECTIONAL = 0
+local ONE_WAY = 1
+
+-- Connector draw parameters
+local CONNECTION_THICKNESS = 0.015
+local CONNECTION_COLOR = { 0.4, 0.4, 0.4, 1 }
+local DIRECTIONAL_ARROW_DISTANCE = 3.5
+local ARROW_ARM_LENGTH = 0.9
+local ARROW_ANGLE = 25
+
+-- Height to draw the connector lines, places them just above the table and always below cards
+local CONNECTION_LINE_Y = 1.529
-- we use this to turn off collision handling until onLoad() is complete
-COLLISION_ENABLED = false
+local collisionEnabled = false
-- used for recreating the link to a custom data helper after image change
customDataHelper = nil
@@ -22,10 +36,24 @@ local SHIFT_EXCLUSION = {
["f182ee"] = true,
["721ba2"] = true
}
+local LOC_LINK_EXCLUDE_SCENARIOS = {
+ ["Devil Reef"] = true,
+ ["The Witching Hour"] = true,
+}
local tokenManager = require("core/token/TokenManager")
local INVESTIGATOR_COUNTER_GUID = "f182ee"
local PLAY_AREA_ZONE_GUID = "a2f932"
+
+local clueData = {}
+local spawnedLocationGUIDs = {}
+
+local locations = { }
+local locationConnections = { }
+local draggingGuids = { }
+
+local locationData
+
local currentScenario
---------------------------------------------------------
@@ -34,17 +62,19 @@ local currentScenario
function onSave()
return JSON.encode({
- currentScenario = currentScenario
+ trackedLocations = locations,
+ currentScenario = currentScenario,
})
end
function onLoad(saveState)
-- records locations we have spawned clues for
- local saveData = JSON.decode(saveState) or {}
- currentScenario = saveData.currentScenario
+ local save = JSON.decode(saveState) or { }
+ locations = save.trackedLocations or { }
+ currentScenario = save.currentScenario
self.interactable = DEBUG
- Wait.time(function() COLLISION_ENABLED = true end, 1)
+ Wait.time(function() collisionEnabled = true end, 1)
end
function log(message)
@@ -61,17 +91,20 @@ function updateLocations(args)
end
end
-function onCollisionEnter(collision_info)
- local obj = collision_info.collision_object
+function onCollisionEnter(collisionInfo)
+ local obj = collisionInfo.collision_object
local objType = obj.name
-- only continue for cards
- if not COLLISION_ENABLED or (objType ~= "Card" and objType ~= "CardCustom") then return end
+ if not collisionEnabled or (objType ~= "Card" and objType ~= "CardCustom") then return end
-- check if we should spawn clues here and do so according to playercount
- if shouldSpawnTokens(obj) then
- tokenManager.spawnForCard(obj)
+ local card = collisionInfo.collision_object
+ if shouldSpawnTokens(card) then
+ tokenManager.spawnForCard(card)
end
+ draggingGuids[card.getGUID()] = nil
+ maybeTrackLocation(card)
end
function shouldSpawnTokens(card)
@@ -85,6 +118,267 @@ function shouldSpawnTokens(card)
or metadata.weakness
end
+function onCollisionExit(collisionInfo)
+ maybeUntrackLocation(collisionInfo.collision_object)
+end
+
+-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well
+function onObjectDestroy(object)
+ maybeUntrackLocation(object)
+end
+
+function onObjectPickUp(player, object)
+ -- onCollisionExit fires first, so we have to check the card to see if it's a location we should
+ -- be tracking
+ if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= "" then
+ local pickedUpGuid = object.getGUID()
+ local metadata = JSON.decode(object.getGMNotes())
+ if (metadata.type == "Location") then
+ draggingGuids[pickedUpGuid] = metadata
+ rebuildConnectionList()
+ end
+ end
+end
+
+function onUpdate()
+ -- Due to the frequence of onUpdate calls, ensure that we only process any changes to the
+ -- connection list once, and only redraw once
+ local needsConnectionRebuild = false
+ local needsConnectionDraw = false
+ for guid, _ in pairs(draggingGuids) do
+ local obj = getObjectFromGUID(guid)
+ if obj == nil or not isInPlayArea(obj) then
+ draggingGuids[guid] = nil
+ needsConnectionRebuild = true
+ end
+ -- Even if the last location left the play area, need one last draw to clear the lines
+ needsConnectionDraw = true
+ end
+ if (needsConnectionRebuild) then
+ rebuildConnectionList()
+ end
+ if needsConnectionDraw then
+ drawConnections()
+ end
+end
+
+-- Checks the given card and adds it to the list of locations tracked for connection purposes.
+-- A card will be added to the tracking if it is a location in the play area (based on centerpoint).
+-- @param A card object, possibly a location.
+function maybeTrackLocation(card)
+ -- Collision checks for any part of the card overlap, but our other tracking is centerpoint
+ -- Ignore any collision where the centerpoint isn't in the area
+ if showLocationLinks() and isInPlayArea(card) then
+ local metadata = JSON.decode(card.getGMNotes()) or { }
+ if metadata.type == "Location" then
+ locations[card.getGUID()] = metadata
+ rebuildConnectionList()
+ drawConnections()
+ end
+ end
+end
+
+-- Stop tracking a location for connection drawing. This should be called for both collision exit
+-- and destruction, as a destroyed object does not trigger collision exit. An object can also be
+-- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will
+-- be cleared in the next onUpdate() cycle.
+-- @param card Card to (maybe) stop tracking
+function maybeUntrackLocation(card)
+ -- Locked objects no longer collide (hence triggering an exit event) but are still in the play
+ -- area. If the object is now locked, don't remove it.
+ if locations[card.getGUID()] ~= nil and not card.locked then
+ locations[card.getGUID()] = nil
+ rebuildConnectionList()
+ drawConnections()
+ end
+end
+
+-- Builds a list of GUID to GUID connection information based on the currently tracked locations.
+-- This will update the connection information and store it in the locationConnections data member,
+-- but does not draw those connections. This should often be followed by a call to
+-- drawConnections()
+function rebuildConnectionList()
+ if not showLocationLinks() then
+ locationConnections = { }
+ return
+ end
+
+ local iconCardList = { }
+
+ -- Build a list of cards with each icon as their location ID
+ for cardId, metadata in pairs(draggingGuids) do
+ buildLocListByIcon(cardId, iconCardList)
+ end
+ for cardId, metadata in pairs(locations) do
+ buildLocListByIcon(cardId, iconCardList)
+ end
+
+ -- Pair up all the icons
+ locationConnections = { }
+ for cardId, metadata in pairs(draggingGuids) do
+ buildConnection(cardId, iconCardList)
+ end
+ for cardId, metadata in pairs(locations) do
+ if draggingGuids[cardId] == nil then
+ buildConnection(cardId, iconCardList)
+ end
+ end
+end
+
+-- Extracts the card's icon string into a list of individual location icons
+-- @param cardID GUID of the card to pull the icon data from
+-- @param iconCardList A table of icon->GUID list. Mutable, will be updated by this method
+function buildLocListByIcon(cardId, iconCardList)
+ local card = getObjectFromGUID(cardId)
+ local locData = getLocationData(card)
+ if locData ~= nil and locData.icons ~= nil then
+ for icon in string.gmatch(locData.icons, "%a+") do
+ if iconCardList[icon] == nil then
+ iconCardList[icon] = { }
+ end
+ table.insert(iconCardList[icon], card.getGUID())
+ end
+ end
+end
+
+-- Builds the connections for the given cardID by finding matching icons and adding them to the
+-- Playarea's locationConnections table.
+-- @param cardId GUID of the card to build the connections for
+-- @param iconCardList A table of icon->GUID List. Used to find matching icons for connections.
+function buildConnection(cardId, iconCardList)
+ local card = getObjectFromGUID(cardId)
+ local locData = getLocationData(card)
+ if locData ~= nil and locData.connections ~= nil then
+ locationConnections[card.getGUID()] = { }
+ for icon in string.gmatch(locData.connections, "%a+") do
+ if iconCardList[icon] ~= nil then
+ for _, connectedGuid in ipairs(iconCardList[icon]) do
+ -- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way
+ if locationConnections[connectedGuid] ~= nil
+ and locationConnections[connectedGuid][card.getGUID()] ~= nil then
+ locationConnections[connectedGuid][card.getGUID()] = BIDIRECTIONAL
+ else
+ locationConnections[card.getGUID()][connectedGuid] = ONE_WAY
+ end
+ end
+ end
+ end
+ end
+end
+
+-- Helper method to extract the location metadata from a card based on whether it's front or back
+-- is showing.
+-- @param card Card object to extract data from
+-- @return Table with either the locationFront or locationBack metadata structure, or nil if the
+-- metadata doesn't exist
+function getLocationData(card)
+ if card == nil then
+ return nil
+ end
+ if card.is_face_down then
+ return JSON.decode(card.getGMNotes()).locationBack
+ else
+ return JSON.decode(card.getGMNotes()).locationFront
+ end
+end
+
+-- Draws the lines for connections currently in locationConnections.
+function drawConnections()
+ if not showLocationLinks() then
+ locationConnections = { }
+ return
+ end
+ local cardConnectionLines = { }
+
+ for originGuid, targetGuids in pairs(locationConnections) do
+ -- Objects should reliably exist at this point, but since this can be called during onUpdate the
+ -- object checks are conservative just to make sure.
+ local origin = getObjectFromGUID(originGuid)
+ if origin != nil then
+ for targetGuid, direction in pairs(targetGuids) do
+ local target = getObjectFromGUID(targetGuid)
+ if target != nil then
+ if direction == BIDIRECTIONAL then
+ addBidirectionalVector(origin, target, cardConnectionLines)
+ elseif direction == ONE_WAY then
+ addOneWayVector(origin, target, cardConnectionLines)
+ end
+ end
+ end
+ end
+ end
+ self.setVectorLines(cardConnectionLines)
+end
+
+-- Draws a bidirectional location connection between the two cards, adding the lines to do so to the
+-- given lines list.
+-- @param card1 One of the card objects to connect
+-- @param card2 The other card object to connect
+-- @param lines List of vector line elements. Mutable, will be updated to add this connector
+function addBidirectionalVector(card1, card2, lines)
+ local cardPos1 = card1.getPosition()
+ local cardPos2 = card2.getPosition()
+ cardPos1.y = CONNECTION_LINE_Y
+ cardPos2.y = CONNECTION_LINE_Y
+ local pos1 = self.positionToLocal(cardPos1)
+ local pos2 = self.positionToLocal(cardPos2)
+ table.insert(lines, {
+ points = { pos1, pos2 },
+ color = CONNECTION_COLOR,
+ thickness = CONNECTION_THICKNESS,
+ })
+end
+
+-- Draws a one-way location connection between the two cards, adding the lines to do so to the
+-- given lines list. Arrows will point towards the target card.
+-- @param origin Origin card in the connection
+-- @param target Target card object to connect
+-- @param lines List of vector line elements. Mutable, will be updated to add this connector
+function addOneWayVector(origin, target, lines)
+ -- Start with the BiDi then add the arrow lines to it
+ addBidirectionalVector(origin, target, lines)
+ local originPos = origin.getPosition()
+ local targetPos = target.getPosition()
+ originPos.y = CONNECTION_LINE_Y
+ targetPos.y = CONNECTION_LINE_Y
+
+ -- Calculate card distance to be closer for horizontal positions than vertical, since cards are
+ -- taller than they are wide
+ local heading = Vector(originPos):sub(targetPos):heading("y")
+ local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading)))
+
+ -- Calculate the three possible arrow positions. These are offset by half the arrow length to
+ -- make them visually balanced by keeping the arrows centered, not tracking the point
+ local midpoint = Vector(originPos):add(targetPos):scale(Vector(0.5, 0.5, 0.5)):moveTowards(targetPos, ARROW_ARM_LENGTH / 2)
+ local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2)
+ local closeToTarget = Vector(targetPos):moveTowards(originPos, distanceFromCard - ARROW_ARM_LENGTH / 2)
+
+ if (originPos:distance(closeToOrigin) > originPos:distance(closeToTarget)) then
+ addArrowLines(midpoint, originPos, lines)
+ else
+ addArrowLines(closeToOrigin, originPos, lines)
+ addArrowLines(closeToTarget, originPos, lines)
+ end
+end
+
+-- Draws an arrowhead at the given position.
+-- @param arrowheadPosition Centerpoint of the arrowhead to draw (NOT the tip of the arrow)
+-- @param originPos Origin point of the connection, used to position the arrow arms
+-- @param lines List of vector line elements. Mutable, will be updated to add this arrow
+function addArrowLines(arrowheadPos, originPos, lines)
+ local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", -1 * ARROW_ANGLE):add(arrowheadPos)
+ local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", ARROW_ANGLE):add(arrowheadPos)
+
+ local head = self.positionToLocal(arrowheadPos)
+ local arm1 = self.positionToLocal(arrowArm1)
+ local arm2 = self.positionToLocal(arrowArm2)
+ table.insert(lines, {
+ points = { arm1, head, arm2},
+ color = CONNECTION_COLOR,
+ thickness = CONNECTION_THICKNESS
+ })
+end
+
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'
---@param playerColor String Color of the player requesting the shift. Used solely to send an error
@@ -126,6 +420,20 @@ function getInvestigatorCount()
return investigatorCounter.getVar("val")
end
+-- Check to see if the given object is within the bounds of the play area, based solely on the X and
+-- Z coordinates, ignoring height
+-- @param object Object to check
+-- @return True if the object is inside the play area
+function isInPlayArea(object)
+ local bounds = self.getBounds()
+ local position = object.getPosition()
+ -- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up
+ local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2}
+ local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2}
+
+ return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z
+end
+
-- Reset the play area's tracking of which cards have had tokens spawned.
function resetSpawnedCards()
spawnedLocationGUIDs = {}
@@ -133,4 +441,11 @@ end
function onScenarioChanged(scenarioName)
currentScenario = scenarioName
+ if not showLocationLinks() then
+ broadcastToAll("Automatic location connections not available for this scenario")
+ end
+end
+
+function showLocationLinks()
+ return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario]
end
diff --git a/src/core/token/TokenManager.ttslua b/src/core/token/TokenManager.ttslua
index 223b61e3..256aa9c4 100644
--- a/src/core/token/TokenManager.ttslua
+++ b/src/core/token/TokenManager.ttslua
@@ -149,8 +149,12 @@ do
-- spawned state object rather than spawning multiple tokens
---@param shiftDown An offset for the z-value of this group of tokens
TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown)
+ local optionPanel = Global.getTable("optionPanel")
+
if tokenType == "damage" or tokenType == "horror" then
TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
+ elseif tokenType == "resource" and optionPanel["useResourceCounters"] then
+ TokenManager.spawnResourceCounterToken(card, tokenCount)
else
TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown)
end
@@ -184,6 +188,14 @@ do
end)
end
+ TokenManager.spawnResourceCounterToken = function(card, tokenCount)
+ local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
+ local rot = card.getRotation()
+ TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
+ spawned.call("updateVal", tokenCount)
+ end)
+ end
+
-- Spawns a number of tokens.
---@param tokenType String type of token to spawn, valid values are resource", "doom", or "clue".
-- Other types should use spawnCounterToken()
@@ -248,6 +260,8 @@ do
else
rot.y = 270
end
+
+ tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,
diff --git a/src/playercards/AllCardsBag.ttslua b/src/playercards/AllCardsBag.ttslua
index a59a1cb6..a5954e2d 100644
--- a/src/playercards/AllCardsBag.ttslua
+++ b/src/playercards/AllCardsBag.ttslua
@@ -7,6 +7,8 @@ local WEAKNESS_CHECK_Z = 37
local cardIdIndex = { }
local classAndLevelIndex = { }
local basicWeaknessList = { }
+local uniqueWeaknessList = { }
+local cycleIndex = { }
local indexingDone = false
local allowRemoval = false
@@ -45,7 +47,9 @@ function clearIndexes()
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
@@ -121,27 +125,27 @@ function buildSupplementalIndexes()
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 == cardMetadata.id then
-- Add card to the basic weakness list, if appropriate. Some weaknesses have
-- multiple copies, and are added multiple times
- if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then
+ if cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil then
+ table.insert(uniqueWeaknessList, cardMetadata.id)
for i = 1, cardMetadata.basicWeaknessCount do
table.insert(basicWeaknessList, cardMetadata.id)
+ end
end
- end
- table.sort(basicWeaknessList, cardComparator)
- -- Add the card to the appropriate class and level indexes
- local isGuardian = false
- local isSeeker = false
- local isMystic = false
- local isRogue = false
- local isSurvivor = false
- local isNeutral = false
- local upgradeKey
- -- Excludes signature cards (which have no class or level) and alternate
- -- ID entries
- if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
+ -- Add the card to the appropriate class and level indexes
+ local isGuardian = false
+ local isSeeker = false
+ local isMystic = false
+ local isRogue = false
+ local isSurvivor = false
+ local isNeutral = false
+ local upgradeKey
+ -- Excludes signature cards (which have no class or level) and alternate
+ -- ID entries
+ if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
isGuardian = string.match(cardMetadata.class, "Guardian")
isSeeker = string.match(cardMetadata.class, "Seeker")
isMystic = string.match(cardMetadata.class, "Mystic")
@@ -171,12 +175,32 @@ function buildSupplementalIndexes()
if (isNeutral) then
table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id)
end
+
+ local cycleName = cardMetadata.cycle
+ if cycleName ~= nil then
+ cycleName = string.lower(cycleName)
+ if string.match(cycleName, "return") then
+ cycleName = string.sub(cycleName, 11)
+ end
+ if cycleName == "the night of the zealot" then
+ cycleName = "core"
+ end
+ if cycleIndex[cycleName] == nil then
+ cycleIndex[cycleName] = { }
+ end
+ table.insert(cycleIndex[cycleName], cardMetadata.id)
+ end
end
end
end
for _, indexTable in pairs(classAndLevelIndex) do
table.sort(indexTable, cardComparator)
end
+ for _, indexTable in pairs(cycleIndex) do
+ table.sort(indexTable)
+ end
+ table.sort(basicWeaknessList, cardComparator)
+ table.sort(uniqueWeaknessList, cardComparator)
end
-- Comparison function used to sort the class card bag indexes. Sorts by card
@@ -235,6 +259,14 @@ function getCardsByClassAndLevel(params)
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 { }
+ end
+ return cycleIndex[string.lower(cycleName)]
+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.
-- Parameter array must contain these fields to define the search:
@@ -303,6 +335,10 @@ function getBasicWeaknesses()
return basicWeaknessList
end
+function getUniqueWeaknesses()
+ return uniqueWeaknessList
+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
@@ -322,6 +358,7 @@ function isInPlayArea(object)
return position.x < WEAKNESS_CHECK_X
and position.z < WEAKNESS_CHECK_Z
end
+
function isBasicWeakness(cardMetadata)
return cardMetadata ~= nil
and cardMetadata.weakness
diff --git a/src/playercards/PlayerCardPanel.ttslua b/src/playercards/PlayerCardPanel.ttslua
index d6ad9f29..9b85aa2b 100644
--- a/src/playercards/PlayerCardPanel.ttslua
+++ b/src/playercards/PlayerCardPanel.ttslua
@@ -2,24 +2,49 @@ require("playercards/PlayerCardPanelData")
local spawnBag = require("playercards/spawnbag/SpawnBag")
local arkhamDb = require("arkhamdb/ArkhamDb")
--- TODO: Update when the real UI image is in place
-local BUTTON_WIDTH = 150
-local BUTTON_HEIGHT = 550
+-- Size and position information for the three rows of class buttons
+local CIRCLE_BUTTON_SIZE = 250
+local CLASS_BUTTONS_X_OFFSET = 0.1325
+local INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)
+local LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)
+local UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)
+
+-- Size and position information for the two blocks of other buttons
+local MISC_BUTTONS_X_OFFSET = 0.155
+local WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)
+local OTHER_ROW_START = Vector(0.605, 0.1, 0.666)
+
+-- Size and position information for the Cycle (box) buttons
+local CYCLE_BUTTON_SIZE = 468
+local CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)
+local CYCLE_COLUMN_COUNT = 3
+local CYCLE_BUTTONS_X_OFFSET = 0.267
+local CYCLE_BUTTONS_Z_OFFSET = 0.2665
local ALL_CARDS_BAG_GUID = "15bb07"
+local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }
+local TRANSPARENT = { 0, 0, 0, 0 }
+local STARTER_DECK_MODE_STARTERS = "starters"
+local STARTER_DECK_MODE_CARDS_ONLY = "cards"
+
local FACE_UP_ROTATION = { x = 0, y = 270, z = 0}
local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180}
--- Coordinates to begin laying out cards to match the reserved areas of the
--- table. Cards will lay out horizontally, then create additional rows
+-- Coordinates to begin laying out cards. These vary based on the cards that are being placed
local START_POSITIONS = {
- skill = Vector(58.384, 1.36, 92.4),
- event = Vector(53.229, 1.36, 92.4),
- asset = Vector(40.960, 1.36, 92.4),
- investigator = Vector(60, 1.36, 80)
+ classCards = Vector(58.384, 1.36, 92.4),
+ investigator = Vector(60, 1.36, 86),
+ cycle = Vector(48, 1.36, 92.4),
+ other = Vector(56, 1.36, 86),
+ summonedServitor = Vector(55.5, 1.36, 60.2),
+ randomWeakness = Vector(55, 1.36, 75)
}
+-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out
+local CARD_ROW_OFFSET = 3.7
+local CARD_GROUP_OFFSET = 2
+
-- Position offsets for investigator decks in investigator mode, defines the spacing for how the
-- rows and columns are laid out
local INVESTIGATOR_POSITION_SHIFT_ROW = Vector(-11, 0, 0)
@@ -31,7 +56,21 @@ local INVESTIGATOR_MAX_COLS = 6
local INVESTIGATOR_CARD_OFFSET = Vector(-2.55, 0, 0)
local INVESTIGATOR_SIGNATURE_OFFSET = Vector(-5.75, 0, 0)
-local spawnStarterDecks = false
+local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" }
+local CYCLE_LIST = {
+ "Core",
+ "The Dunwich Legacy",
+ "The Path to Carcosa",
+ "The Forgotten Age",
+ "The Circle Undone",
+ "The Dream-Eaters",
+ "The Innsmouth Conspiracy",
+ "Edge of the Earth",
+ "The Scarlet Keys",
+ "Investigator Packs"
+}
+
+local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
function onSave()
local saveState = {
@@ -48,232 +87,223 @@ function onLoad(savedData)
spawnBag.loadFromSave(saveState.spawnBagState)
end
end
+ createButtons()
+end
- self.createButton({
- label="Guardian", click_function="spawnInvestigatorsGuardian", function_owner=self,
- position={-0.3,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Seeker", click_function="spawnInvestigatorsSeeker", function_owner=self,
- position={0,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Mystic", click_function="spawnInvestigatorsMystic", function_owner=self,
- position={0.3,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Rogue", click_function="spawnInvestigatorsRogue", function_owner=self,
- position={-0.3,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Survivor", click_function="spawnSurvivor", function_owner=self,
- position={0,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Neutral", click_function="spawnNeutral", function_owner=self,
- position={0.3,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
+function createButtons()
+ createInvestigatorButtons()
+ createLevelZeroButtons()
+ createUpgradedButtons()
+ createWeaknessButtons()
+ createOtherButtons()
+ createCycleButtons()
+ createClearButton()
+ -- Create investigator mode buttons last so the indexes are set when we need to update them
+ createInvestigatorModeButtons()
+end
- self.createButton({
- label="Core", click_function="spawnCore", function_owner=self,
- position={-0.3,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Dunwich", click_function="spawnDunwich", function_owner=self,
- position={0,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Carcosa", click_function="spawnCarcosa", function_owner=self,
- position={0.3,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Forgotten Age", click_function="spawnForgottenAge", function_owner=self,
- position={-0.3,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Circle Undone", click_function="spawnCircleUndone", function_owner=self,
- position={0,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Dream Eaters", click_function="spawnDreamEaters", function_owner=self,
- position={0.3,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Innsmouth", click_function="spawnInnsmouth", function_owner=self,
- position={-0.3,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="EotE", click_function="spawnEotE", function_owner=self,
- position={0,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Scarlet Keys", click_function="spawnScarletKeys", function_owner=self,
- position={0.3,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="InvPacks", click_function="spawnInvestigatorDecks", function_owner=self,
- position={-0.3,0.2,0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Investigators", click_function="setInvestigators", function_owner=self,
- position={-0.15,0.2,-0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Starters", click_function="setStarters", function_owner=self,
- position={0.15,0.2,-0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
-
- self.createButton({
- label="L0 Guardian", click_function="spawnBasicGuardian", function_owner=self,
- position={-0.15,0.2,0.3}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Guardian", click_function="spawnUpgradedGuardian", function_owner=self,
- position={0.15,0.2,0.3}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L0 Seeker", click_function="spawnBasicSeeker", function_owner=self,
- position={-0.15,0.2,0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Seeker", click_function="spawnUpgradedSeeker", function_owner=self,
- position={0.15,0.2,0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L0 Mystic", click_function="spawnBasicMystic", function_owner=self,
- position={-0.15,0.2,0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Mystic", click_function="spawnUpgradedGuardian", function_owner=self,
- position={0.15,0.2,0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L0 Rogue", click_function="spawnBasicRogue", function_owner=self,
- position={-0.15,0.2,0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Rogue", click_function="spawnUpgradedRogue", function_owner=self,
- position={0.15,0.2,0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L0 Survivor", click_function="spawnBasicSurvivor", function_owner=self,
- position={-0.15,0.2,0.7}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Survivor", click_function="spawnUpgradedSurvivor", function_owner=self,
- position={0.15,0.2,0.7}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L0 Neutral", click_function="spawnBasicNeutral", function_owner=self,
- position={-0.15,0.2,0.8}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="L1-5 Neutral", click_function="spawnUpgradedNeutral", function_owner=self,
- position={0.15,0.2,0.8}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Clear", click_function="deleteAll", function_owner=self,
- position={0.5,0.2,0.9}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- self.createButton({
- label="Weaknesses", click_function="spawnWeaknesses", function_owner=self,
- position={-0.5,0.2,0.9}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
- font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
- })
- local classList = { "Guardian", "Seeker", "Mystic", "Rogue", "Survivor", "Neutral" }
- for _, className in ipairs(classList) do
- local funcName = "spawnInvestigators"..className
- self.setVar(funcName, function(_, _, _) spawnGroup(className) end)
- funcName = "spawnBasic"..className
- self.setVar(funcName, function(_, _, _) spawnClassCards(className, false) end)
- funcName = "spawnUpgraded"..className
- self.setVar(funcName, function(_, _, _) spawnClassCards(className, true) end)
+function createInvestigatorButtons()
+ local invButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CIRCLE_BUTTON_SIZE,
+ width = CIRCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = INVESTIGATOR_ROW_START:copy()
+ for _, class in ipairs(CLASS_LIST) do
+ invButtonParams.click_function = "spawnInvestigators" .. class
+ invButtonParams.position = buttonPos
+ self.createButton(invButtonParams)
+ buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
+ self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)
end
end
--- TODO: Replace these with something less manual once the full data in in place so we know what
--- keys to use
-function placeCore()
- spawnGroup("Core")
+function createLevelZeroButtons()
+ local l0ButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CIRCLE_BUTTON_SIZE,
+ width = CIRCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = LEVEL_ZERO_ROW_START:copy()
+ for _, class in ipairs(CLASS_LIST) do
+ l0ButtonParams.click_function = "spawnBasic" .. class
+ l0ButtonParams.position = buttonPos
+ self.createButton(l0ButtonParams)
+ buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
+ self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)
+ end
end
-function placeDunwich()
- spawnGroup("Dunwich")
+function createUpgradedButtons()
+ local upgradedButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CIRCLE_BUTTON_SIZE,
+ width = CIRCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = UPGRADED_ROW_START:copy()
+ for _, class in ipairs(CLASS_LIST) do
+ upgradedButtonParams.click_function = "spawnUpgraded" .. class
+ upgradedButtonParams.position = buttonPos
+ self.createButton(upgradedButtonParams)
+ buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
+ self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)
+ end
end
-function placeCarcosa()
- spawnGroup("Carcosa")
+function createWeaknessButtons()
+ local weaknessButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CIRCLE_BUTTON_SIZE,
+ width = CIRCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = WEAKNESS_ROW_START:copy()
+ weaknessButtonParams.click_function = "spawnWeaknesses"
+ weaknessButtonParams.tooltip = "Basic Weaknesses"
+ weaknessButtonParams.position = buttonPos
+ self.createButton(weaknessButtonParams)
+ buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
+ weaknessButtonParams.click_function = "spawnRandomWeakness"
+ weaknessButtonParams.tooltip = "Random Weakness"
+ weaknessButtonParams.position = buttonPos
+ self.createButton(weaknessButtonParams)
end
-function placeForgottenAge()
- spawnGroup("ForgottenAge")
+function createOtherButtons()
+ local otherButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CIRCLE_BUTTON_SIZE,
+ width = CIRCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = OTHER_ROW_START:copy()
+ otherButtonParams.click_function = "spawnBonded"
+ otherButtonParams.tooltip = "Bonded Cards"
+ otherButtonParams.position = buttonPos
+ self.createButton(otherButtonParams)
+ buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
+ otherButtonParams.click_function = "spawnUpgradeSheets"
+ otherButtonParams.tooltip = "Customization Upgrade Sheets"
+ otherButtonParams.position = buttonPos
+ self.createButton(otherButtonParams)
end
-function placeCircleUndone()
- spawnGroup("CircleUndone")
+function createCycleButtons()
+ local cycleButtonParams = {
+ function_owner = self,
+ rotation = Vector(0, 0, 0),
+ height = CYCLE_BUTTON_SIZE,
+ width = CYCLE_BUTTON_SIZE,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ }
+ local buttonPos = CYCLE_BUTTON_START:copy()
+ local rowCount = 0
+ local colCount = 0
+ for _, cycle in ipairs(CYCLE_LIST) do
+ cycleButtonParams.click_function = "spawnCycle" .. cycle
+ cycleButtonParams.position = buttonPos
+ cycleButtonParams.tooltip = cycle
+ self.createButton(cycleButtonParams)
+ self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)
+ colCount = colCount + 1
+ -- If we've reached the end of a row, shift down and back to the first column
+ if colCount >= CYCLE_COLUMN_COUNT then
+ buttonPos = CYCLE_BUTTON_START:copy()
+ rowCount = rowCount + 1
+ colCount = 0
+ buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount
+ if rowCount == 3 then
+ -- Account for centered button on the final row
+ buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
+ end
+ else
+ buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
+ end
+ end
end
-function placeDreamEaters()
- spawnGroup("DreamEaters")
+function createClearButton()
+ self.createButton({
+ function_owner = self,
+ click_function = "deleteAll",
+ position = Vector(0, 0.1, 0.852),
+ rotation = Vector(0, 0, 0),
+ height = 170,
+ width = 750,
+ scale = Vector(0.25, 1, 0.25),
+ color = TRANSPARENT,
+ })
end
-function placeInnsmouth()
- spawnGroup("Innsmouth")
+function createInvestigatorModeButtons()
+ local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
+
+ self.createButton({
+ function_owner = self,
+ click_function = "setCardsOnlyMode",
+ position = Vector(0.251, 0.1, -0.322),
+ rotation = Vector(0, 0, 0),
+ height = 170,
+ width = 760,
+ scale = Vector(0.25, 1, 0.25),
+ color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR
+ })
+ self.createButton({
+ function_owner = self,
+ click_function = "setStarterDeckMode",
+ position = Vector(0.66, 0.1, -0.322),
+ rotation = Vector(0, 0, 0),
+ height = 170,
+ width = 760,
+ scale = Vector(0.25, 1, 0.25),
+ color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT
+ })
+ local checkX = starterMode and 0.52 or 0.11
+ self.createButton({
+ function_owner = self,
+ label = "✓",
+ click_function = "doNothing",
+ position = Vector(checkX, 0.11, -0.317),
+ rotation = Vector(0, 0, 0),
+ height = 0,
+ width = 0,
+ scale = Vector(0.3, 1, 0.3),
+ font_color = { 0, 0, 0 },
+ color = { 1, 1, 1 }
+ })
end
-function placeEotE()
- spawnGroup("EotE")
+function setStarterDeckMode()
+ starterDeckMode = STARTER_DECK_MODE_STARTERS
+ updateStarterModeButtons()
end
-function placeScarletKeys()
- spawnGroup("ScarletKeys")
+function setCardsOnlyMode()
+ starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
+ updateStarterModeButtons()
end
-function placeInvestigatorDecks()
- spawnGroup("InvestigatorDecks")
-end
-
--- UI handler to put the investigator spawn in investigator mode.
-function setInvestigators()
- spawnStarterDecks = false
- printToAll("Spawning investigator piles")
-end
-
--- UI handler to put the investigator spawn in starter deck mode.
-function setStarters()
- spawnStarterDecks = true
- printToAll("Spawning starter decks")
+function updateStarterModeButtons()
+ local buttonCount = #self.getButtons()
+ -- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size
+ self.removeButton(buttonCount - 1)
+ self.removeButton(buttonCount - 2)
+ self.removeButton(buttonCount - 3)
+ createInvestigatorModeButtons()
end
-- Deletes all cards currently placed on the table
@@ -284,10 +314,11 @@ end
-- Spawn an investigator group, based on the current UI setting for either investigators or starter
-- decks.
---@param groupName String. Name of the group to spawn, matching a key in InvestigatorPanelData
-function spawnGroup(groupName)
+function spawnInvestigatorGroup(groupName)
+ local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
spawnBag.recall(true)
Wait.frames(function()
- if spawnStarterDecks then
+ if starterMode then
spawnStarters(groupName)
else
spawnInvestigators(groupName)
@@ -359,13 +390,13 @@ function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneC
return {
{
name = investigatorName.."minicards",
- cards = oneCardOnly and investigatorData.minicards[1] or investigatorData.minicards,
+ cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,
globalPos = position,
rotation = FACE_UP_ROTATION,
},
{
name = investigatorName.."cards",
- cards = oneCardOnly and investigatorData.cards[1] or investigatorData.cards,
+ cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,
globalPos = cardPos,
rotation = FACE_UP_ROTATION,
},
@@ -452,31 +483,34 @@ function placeClassCards(cardClass, isUpgraded)
table.insert(assetList, cardId)
end
end
+ local groupPos = Vector(START_POSITIONS.classCards)
if #skillList > 0 then
spawnBag.spawn({
name = cardClass .. (isUpgraded and "upgraded" or "basic"),
cards = skillList,
- globalPos = START_POSITIONS.skill,
+ globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
+ groupPos.x = groupPos.x - math.ceil(#skillList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #eventList > 0 then
spawnBag.spawn({
name = cardClass .. "event" .. (isUpgraded and "upgraded" or "basic"),
cards = eventList,
- globalPos = START_POSITIONS.event,
+ globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
+ groupPos.x = groupPos.x - math.ceil(#eventList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #assetList > 0 then
spawnBag.spawn({
name = cardClass .. "asset" .. (isUpgraded and "upgraded" or "basic"),
cards = assetList,
- globalPos = START_POSITIONS.asset,
+ globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
@@ -484,26 +518,108 @@ function placeClassCards(cardClass, isUpgraded)
end
end
--- Clears the current cards, and places all basic weaknesses on the table.
-function spawnWeaknesses()
- spawnBag.recall(fast)
+-- Spawns the investigator sets and all cards for the given cycle
+---@param cycle String Name of a cycle, should match the standard used in card metadata
+function spawnCycle(cycle)
+ spawnBag.recall(true)
+ spawnInvestigators(cycle)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local indexReady = allCardsBag.call("isIndexReady")
if (not indexReady) then
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
return
end
- local weaknessIdList = allCardsBag.call("getBasicWeaknesses")
+ local cycleCardList = allCardsBag.call("getCardsByCycle", cycle)
local copiedList = { }
- for i, id in ipairs(weaknessIdList) do
+ for i, id in ipairs(cycleCardList) do
copiedList[i] = id
end
spawnBag.spawn({
- name = "weaknesses",
+ name = "cycle"..cycle,
cards = copiedList,
- globalPos = START_POSITIONS.asset,
+ globalPos = START_POSITIONS.cycle,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
+
+function spawnBonded()
+ spawnBag.recall(true)
+ spawnBag.spawn({
+ name = "bonded",
+ cards = BONDED_CARD_LIST,
+ globalPos = START_POSITIONS.classCards,
+ rotation = FACE_UP_ROTATION,
+ spread = true,
+ spreadCols = 20
+ })
+end
+
+function spawnUpgradeSheets()
+ spawnBag.recall(true)
+ spawnBag.spawn({
+ name = "upgradeSheets",
+ cards = UPGRADE_SHEET_LIST,
+ globalPos = START_POSITIONS.classCards,
+ rotation = FACE_UP_ROTATION,
+ spread = true,
+ spreadCols = 20
+ })
+ spawnBag.spawn({
+ name = "servitor",
+ cards = { "09080-m" },
+ globalPos = START_POSITIONS.summonedServitor,
+ rotation = FACE_UP_ROTATION,
+ })
+end
+
+-- Clears the current cards, and places all basic weaknesses on the table.
+function spawnWeaknesses()
+ spawnBag.recall(true)
+ local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
+ local indexReady = allCardsBag.call("isIndexReady")
+ if (not indexReady) then
+ broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
+ return
+ end
+ local weaknessIdList = allCardsBag.call("getUniqueWeaknesses")
+ local copiedList = { }
+ for i, id in ipairs(weaknessIdList) do
+ copiedList[i] = id
+ end
+ local groupPos = Vector(START_POSITIONS.classCards)
+ spawnBag.spawn({
+ name = "weaknesses",
+ cards = copiedList,
+ globalPos = groupPos,
+ rotation = FACE_UP_ROTATION,
+ spread = true,
+ spreadCols = 20
+ })
+ groupPos.x = groupPos.x - math.ceil(#copiedList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
+ spawnBag.spawn({
+ name = "evolvedWeaknesses",
+ cards = EVOLVED_WEAKNESSES,
+ globalPos = groupPos,
+ rotation = FACE_UP_ROTATION,
+ spread = true,
+ spreadCols = 20
+ })
+end
+
+function spawnRandomWeakness()
+ spawnBag.recall(true)
+ local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
+ local weaknessId = allCardsBag.call("getRandomWeaknessId")
+ if (weaknessId == nil) then
+ broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2})
+ return
+ end
+ spawnBag.spawn({
+ name = "randomWeakness",
+ cards = { weaknessId },
+ globalPos = START_POSITIONS.randomWeakness,
+ rotation = FACE_UP_ROTATION,
+ })
+end
diff --git a/src/playercards/PlayerCardPanelData.ttslua b/src/playercards/PlayerCardPanelData.ttslua
index 62e13e6e..6d186e55 100644
--- a/src/playercards/PlayerCardPanelData.ttslua
+++ b/src/playercards/PlayerCardPanelData.ttslua
@@ -1,3 +1,45 @@
+BONDED_CARD_LIST = {
+ "05314", -- Soothing Melody
+ "06277", -- Wish Eater
+ "06019", -- Bloodlust
+ "06022", -- Pendant of the Queen
+ "05317", -- Blood-rite
+ "06113", -- Essence of the Dream
+ "06028", -- Stars Are Right
+ "06025", -- Guardian of the Crystallizer
+ "06283", -- Unbound Beast
+ "06032", -- Zeal
+ "06031", -- Hope
+ "06033", -- Augur
+ "06331", -- Dream Parasite
+ "06015a", -- Dream-Gate
+}
+
+UPGRADE_SHEET_LIST = {
+ "09040-c", -- Alchemical Distillation
+ "09023-c", -- Custom Modifications
+ "09059-c", -- Damning Testimony
+ "09041-c", -- Emperical Hypothesis
+ "09060-c", -- Friends in Low Places
+ "09101-c", -- Grizzled
+ "09061-c", -- Honed Instinct
+ "09021-c", -- Hunter's Armor
+ "09119-c", -- Hyperphysical Shotcaster
+ "09079-c", -- Living Ink
+ "09100-c", -- Makeshift Trap
+ "09099-c", -- Pocket Multi Tool
+ "09081-c", -- Power Word
+ "09022-c", -- Runic Axe
+ "09080-c", -- Summoned Servitor
+ "09042-c", -- Raven's Quill
+}
+
+EVOLVED_WEAKNESSES = {
+ "04039",
+ "04041",
+ "04042",
+}
+
------------------ START INVESTIGATOR DATA DEFINITION ------------------
INVESTIGATOR_GROUPS = {
Guardian = {
@@ -13,10 +55,6 @@ INVESTIGATOR_GROUPS = {
"D2",
"R3",
"D3",
- "R4",
- "D4",
- "R5",
- "D5",
},
}
@@ -25,13 +63,13 @@ INVESTIGATORS["Roland Banks"] = {
cards = { "01001", "01001-promo", "01001-p", "01001-pf", "01001-pb", },
minicards = { "01001-m", "01001-promo-m", },
signatures = { "01006", "01007", "90030", "90031", },
- starterDeck = "1462",
+ starterDeck = "2624931",
}
INVESTIGATORS["Daisy Walker"] = {
cards = { "01002", "01002-p", "01002-pf", "01002-pb", },
minicards = { "01002-m", },
signatures = { "01008", "01009", "90002", "90003" },
- starterDeck = "42652",
+ starterDeck = "2624938",
}
------------------ END INVESTIGATOR DATA DEFINITION ------------------
INVESTIGATORS["R2"] = INVESTIGATORS["Roland Banks"]
diff --git a/src/playermat/Playmat.ttslua b/src/playermat/Playmat.ttslua
index d98e3a31..ca748ec7 100644
--- a/src/playermat/Playmat.ttslua
+++ b/src/playermat/Playmat.ttslua
@@ -409,12 +409,18 @@ function replenishTokens(card, count, replenish)
-- get current amount of resource tokens on the card
local search = searchArea(cardPos, { 2.5, 0.5, 3.5 })
+ local clickableResourceCounter = nil
local foundTokens = 0
+
for _, obj in ipairs(search) do
local obj = obj.hit_object
if obj.getCustomObject().image == "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/" then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
+ elseif obj.getName() == "Resource Counter" then
+ foundTokens = obj.getVar("val")
+ clickableResourceCounter = obj
+ break
end
end
@@ -434,7 +440,12 @@ function replenishTokens(card, count, replenish)
local newCount = foundTokens + replenish
if newCount > count then newCount = count end
- tokenManager.spawnTokenGroup(card, "resource", newCount)
+
+ if clickableResourceCounter then
+ clickableResourceCounter.call("updateVal", newCount)
+ else
+ tokenManager.spawnTokenGroup(card, "resource", newCount)
+ end
end
function syncCustomizableMetadata(card)
@@ -663,7 +674,7 @@ function clickableClues(showCounter)
local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
- TokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
+ tokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
end
end
end
diff --git a/src/util/TokenRemover.ttslua b/src/util/TokenRemover.ttslua
index 9ed755f9..269e7af2 100644
--- a/src/util/TokenRemover.ttslua
+++ b/src/util/TokenRemover.ttslua
@@ -1,72 +1,74 @@
----
---- Generated by EmmyLua(https://github.com/EmmyLua)
---- Created by Whimsical.
---- DateTime: 2021-02-02 9:41 a.m.
----
-
local zone = nil
+local CHAOS_TOKEN_NAMES = {
+ ["Elder Sign"] = true,
+ ["+1"] = true,
+ ["0"] = true,
+ ["-1"] = true,
+ ["-2"] = true,
+ ["-3"] = true,
+ ["-4"] = true,
+ ["-5"] = true,
+ ["-6"] = true,
+ ["-7"] = true,
+ ["-8"] = true,
+ ["Skull"] = true,
+ ["Cultist"] = true,
+ ["Tablet"] = true,
+ ["Elder Thing"] = true,
+ ["Auto-fail"] = true,
+ ["Bless"] = true,
+ ["Curse"] = true,
+ ["Frost"] = true
+}
--- Forward Declaration
----@param is_enabled boolean
-local setMenu = function(is_enabled) end
-
-local function enable()
- if self.held_by_color~=nil then return end
- local position = self:getPosition()
- local rotation = self:getRotation()
- local scale = self:getScale()
-
- zone = spawnObject {
- type = "ScriptingTrigger",
- position = Vector(position.x, position.y+25+(bit32.rshift(scale.y, 1))+0.41, position.z),
- rotation = rotation,
- scale = Vector(scale.x*2, 50, scale.z*2),
- sound = true,
- snap_to_grid = true
- }
-
- setMenu(false)
-end
-
-local function disable()
- if zone~=nil then zone:destruct() end
- setMenu(true)
-end
-
----@param is_enabled boolean
-setMenu = function(is_enabled)
- self:clearContextMenu()
- if is_enabled then
- self:addContextMenuItem("Enable", enable, false)
- else
- self:addContextMenuItem("Disable", disable, false)
- end
-end
-
-function onLoad(save_state)
- if save_state=="" then return end
- local data = JSON.decode(save_state)
- zone = getObjectFromGUID(data.zone)
- setMenu(zone==nil)
-end
-
+-- general code
function onSave()
- return JSON.encode {
- zone = zone and zone:getGUID() or nil
- }
+ return JSON.encode(zone and zone.getGUID() or nil)
end
----@param entering TTSObject
----@param object TTSObject
-function onObjectEnterScriptingZone(entering , object)
- if zone~=entering then return end
- if object==self then return end
- if object.type=="Deck" or object.type=="Card" then return end
-
- object:destruct()
+function onLoad(savedData)
+ if savedData ~= "" and savedData ~= nil then
+ zone = getObjectFromGUID(JSON.decode(savedData))
+ end
+ setMenu(zone == nil)
end
----@param color string
-function onPickUp(color)
- disable()
+-- context menu functions
+function enable()
+ local scale = self.getScale()
+
+ zone = spawnObject({
+ type = "ScriptingTrigger",
+ position = self.getPosition() + Vector(0, 2.5 + 0.11, 0),
+ rotation = self.getRotation(),
+ scale = { scale.x * 2, 5, scale.z * 2 }
+ })
+
+ setMenu(false)
+end
+
+function disable()
+ if zone ~= nil then zone.destruct() end
+ setMenu(true)
+end
+
+-- core functions
+function setMenu(isEnabled)
+ self.clearContextMenu()
+ if isEnabled then
+ self.addContextMenuItem("Enable", enable)
+ else
+ self.addContextMenuItem("Disable", disable)
+ end
+end
+
+function onObjectEnterScriptingZone(entering, object)
+ if zone ~= entering then return end
+ if object == self or object.type == "Deck" or object.type == "Card" then return end
+ if CHAOS_TOKEN_NAMES[object.getName()] then return end
+ object.destruct()
+end
+
+function onPickUp()
+ disable()
end
diff --git a/xml/Global.xml b/xml/Global.xml
index 6ba20511..dc15bced 100644
--- a/xml/Global.xml
+++ b/xml/Global.xml
@@ -104,4 +104,17 @@
+
+
+
+
diff --git a/xml/OptionPanel.xml b/xml/OptionPanel.xml
index afb322b1..db0912e8 100644
--- a/xml/OptionPanel.xml
+++ b/xml/OptionPanel.xml
@@ -36,7 +36,7 @@
+ preferredHeight="70"/>
|
- Only cards with the tag "Asset" will snap (official cards are supported by default).
Disable this if you are having issues with custom content.
+ Only cards with the tag "Asset" will snap (official cards are supported by default). Disable this if you are having issues with custom content.
|
@@ -142,6 +142,34 @@
|
+
+
+
+
+
+ This enables spawning of clickable resource tokens for player cards.
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ Fade in the name of the scenario for 2 seconds when placing down a scenario.
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ Provides a card-sized bag for cards that are attached to other cards (e.g. Backpack).
+
+ |
+
+
+ |
+
+
@@ -221,6 +263,48 @@
onValueChanged="onClick_toggleOption(showNavigationOverlay)"/>
|
+
+
+
+
+
+
+ Displays in a "Choose Your Own Adventure" style redesigned campaign guides.
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ Places a tool that displays custom playmat images for all cycles in a gallery-like fashion.
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ This allows moving all objects on the main playmat in a chosen direction.
+
+ |
+
+
+ |
+