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"/> Enable snap tags - 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 @@ + + + + + Use clickable resource counters + This enables spawning of clickable resource tokens for player cards. + + + + + + + + + + + + Show Scenario Title on Setup + Fade in the name of the scenario for 2 seconds when placing down a scenario. + + + + + + + @@ -185,7 +213,7 @@ Hand Helper - Never count your hand cards again! This tool does that for you and can even take "Dream-Enhancing Serum" into account. Also includes a button for randomly discard a card. + Never count your hand cards again! This tool does that for you and additionally enables easy discarding of random cards. @@ -208,6 +236,20 @@ + + + + + Attachment Helper + Provides a card-sized bag for cards that are attached to other cards (e.g. Backpack). + + + + + + + @@ -221,6 +263,48 @@ onValueChanged="onClick_toggleOption(showNavigationOverlay)"/> + + + + + + CYOA Campaign Guides + Displays in a "Choose Your Own Adventure" style redesigned campaign guides. + + + + + + + + + + + + Custom Playmat Images + Places a tool that displays custom playmat images for all cycles in a gallery-like fashion. + + + + + + + + + + + + Displacement Tool + This allows moving all objects on the main playmat in a chosen direction. + + + + + +