From c20ed92dd3f80a40a85b5aa555fd7ad895efe1a4 Mon Sep 17 00:00:00 2001 From: Chr1Z93 Date: Sun, 1 Mar 2026 19:02:55 +0100 Subject: [PATCH] Improve hotkey for connection drawing --- config.json | 1 - objects/DrawingTool.280086.json | 57 -------------- src/playarea/PlayArea.ttslua | 108 ++++++++++++++++++++++++-- src/util/ConnectionDrawingTool.ttslua | 95 ---------------------- 4 files changed, 102 insertions(+), 159 deletions(-) delete mode 100644 objects/DrawingTool.280086.json delete mode 100644 src/util/ConnectionDrawingTool.ttslua diff --git a/config.json b/config.json index 6bedd7302..ce871d66b 100644 --- a/config.json +++ b/config.json @@ -120,7 +120,6 @@ "LeadInvestigator.acaa93", "DeckImporter.a28140", "Configuration.03804b", - "DrawingTool.280086", "PlayAreaImageSwapper.b7b45b", "AllPlayerCards.15bb07", "InvestigatorSkillTracker.af7ed7", diff --git a/objects/DrawingTool.280086.json b/objects/DrawingTool.280086.json deleted file mode 100644 index 4227cecd7..000000000 --- a/objects/DrawingTool.280086.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomToken": { - "MergeDistancePixels": 15, - "Stackable": false, - "StandUp": false, - "Thickness": 0.1 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "", - "ImageURL": "https://steamusercontent-a.akamaihd.net/ugc/1850441528392677845/F9F54E5144735C7DDFFF88E5D706D0750BA08FBA/", - "WidthScale": 0 - }, - "Description": "Enables drawing lines between objects with Numpad 0.\n\nLong press it to only draw the lines to the hovered object.\n\nSee context menu for additional information.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "280086", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "require(\"util/ConnectionDrawingTool\")", - "LuaScriptState": "{\"connections\":[]}", - "MeasureMovement": false, - "Name": "Custom_Token", - "Nickname": "Drawing Tool", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 78, - "posY": 1.195, - "posZ": 7.589, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.14, - "scaleY": 1, - "scaleZ": 0.14 - }, - "Value": 0, - "XmlUI": "" -} diff --git a/src/playarea/PlayArea.ttslua b/src/playarea/PlayArea.ttslua index 63d964141..f6d9a6e01 100644 --- a/src/playarea/PlayArea.ttslua +++ b/src/playarea/PlayArea.ttslua @@ -41,6 +41,7 @@ local SHIFT_OFFSETS = { local locations = {} local locationConnections = {} +local manualConnections = {} local draggingGuids = {} local missingData = {} local collisionEnabled = false @@ -55,7 +56,8 @@ function updateSave() trackedLocations = locations, connectionColor = connectionColor, connectionsEnabled = connectionsEnabled, - excludedScenario = excludedScenario + excludedScenario = excludedScenario, + manualConnections = manualConnections }) end @@ -66,6 +68,7 @@ function onLoad(savedData) connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 } connectionsEnabled = loadedData.connectionsEnabled excludedScenario = loadedData.excludedScenario + manualConnections = loadedData.manualConnections or {} end Wait.time(function() collisionEnabled = true end, 0.1) @@ -75,6 +78,72 @@ end -- TTS event handling --------------------------------------------------------- +-- use scripting button 10 (numpad 0) to add manual connections +function onScriptingButtonDown(index, playerColor) + if index ~= 10 then return end + + -- filter hovered and selected objects for valid locations (card with tag) + local targetCards = {} + + for _, obj in ipairs(Player[playerColor].getSelectedObjects()) do + if isValidLocation(obj) then + table.insert(targetCards, obj) + end + end + + -- if nothing or 1 card is selected, toggle its connections + if #targetCards <= 1 then + local targetObj = targetCards[1] or Player[playerColor].getHoverObject() + + if targetObj and isValidLocation(targetObj) then + local guid = targetObj.getGUID() + + -- look at locationConnections because it contains the final result of Auto + Manual logic + local currentLinks = locationConnections[guid] or {} + + for otherGuid, _ in pairs(currentLinks) do + -- We only need to store the manual toggle once. + -- Our XOR rebuild logic handles the rest. + local low = guid < otherGuid and guid or otherGuid + local high = guid < otherGuid and otherGuid or guid + + manualConnections[low] = manualConnections[low] or {} + manualConnections[low][high] = not manualConnections[low][high] + end + broadcastToColor("Toggled all connections for " .. targetObj.getName(), playerColor, "Green") + + rebuildConnectionList() + drawBaseConnections() + else + -- still nothing? + broadcastToColor("It seems like there is no valid card among your selection / hover.", playerColor, "Orange") + end + elseif #targetCards > 10 then + broadcastToColor("This only supports up to 10 cards for performance reasons.", playerColor, "Orange") + else + broadcastToColor("Toggled connections for " .. #targetCards .. " cards.", playerColor, "Green") + + -- toggle connections for each target card (to each other target card) + for i, card in ipairs(targetCards) do + local cardGuid = card.getGUID() + for j, otherCard in ipairs(targetCards) do + if i < j then -- Only process each pair once + local otherGuid = otherCard.getGUID() + manualConnections[cardGuid] = manualConnections[cardGuid] or {} + manualConnections[cardGuid][otherGuid] = not manualConnections[cardGuid][otherGuid] + end + end + end + + rebuildConnectionList() + drawBaseConnections() + end +end + +function isValidLocation(obj) + return obj.type == "Card" and obj.hasTag("Location") +end + function onCollisionEnter(collisionInfo) if not collisionEnabled then return end @@ -122,6 +191,8 @@ end function onObjectPickUp(_, object) if object.type ~= "Card" then return end + object.setVectorLines({}) + -- onCollisionExit USUALLY 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) then local metadata = JSON.decode(object.getGMNotes()) or {} @@ -146,7 +217,7 @@ end -- Due to the frequence of onUpdate calls, ensure that we only process any changes once function onUpdate() local needsConnectionRebuild = false - local needsConnectionDraw = false + local needsConnectionDraw = false for guid, _ in pairs(draggingGuids) do local obj = getObjectFromGUID(guid) if obj == nil or not isInPlayArea(obj) then @@ -282,6 +353,27 @@ function rebuildConnectionList() buildConnection(cardId, iconCardList, metadata) end end + + -- Handle manual connections (XOR logic to be able to cancel existing connections) + for originGuid, targets in pairs(manualConnections) do + for targetGuid, isEnabled in pairs(targets) do + if isEnabled then + locationConnections[originGuid] = locationConnections[originGuid] or {} + locationConnections[targetGuid] = locationConnections[targetGuid] or {} + + -- If a connection already exists, REMOVE it + if locationConnections[originGuid][targetGuid] ~= nil or + locationConnections[targetGuid][originGuid] ~= nil then + locationConnections[originGuid][targetGuid] = nil + locationConnections[targetGuid][originGuid] = nil + else + -- If no connection exists, ADD it + locationConnections[originGuid][targetGuid] = BIDIRECTIONAL + locationConnections[targetGuid][originGuid] = BIDIRECTIONAL + end + end + end + end end -- Extracts the card's icon string into a list of individual location icons @@ -341,10 +433,14 @@ function drawBaseConnections() -- 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 draggingGuids[originGuid] == nil and origin ~= nil then + + -- Check if origin is being held by a player + if origin ~= nil and draggingGuids[originGuid] == nil and origin.held_by_color == nil then for targetGuid, direction in pairs(targetGuids) do local target = getObjectFromGUID(targetGuid) - if draggingGuids[targetGuid] == nil and target ~= nil then + + -- Check if target is being held by a player + if target ~= nil and draggingGuids[targetGuid] == nil and target.held_by_color == nil then -- Since we process the full list, we're guaranteed to hit any ONE_WAY connections later -- so we can ignore INCOMING_ONE_WAY if direction == BIDIRECTIONAL then @@ -742,8 +838,8 @@ end -- Converts grid coordinates (e.g., {x=1.5, y=-2}) back to world position function gridToWorld(grid) - local localX = CENTER_X + (grid.x or grid[1])* STEP_X * 2 - local localZ = CENTER_Z - (grid.y or grid[2])* STEP_Z * 2 + local localX = CENTER_X + (grid.x or grid[1]) * STEP_X * 2 + local localZ = CENTER_Z - (grid.y or grid[2]) * STEP_Z * 2 local pos = self.positionToWorld({ x = localX, y = 0.1, z = localZ }) return MathLib.roundVector(pos, 3) end diff --git a/src/util/ConnectionDrawingTool.ttslua b/src/util/ConnectionDrawingTool.ttslua deleted file mode 100644 index f68a0ef73..000000000 --- a/src/util/ConnectionDrawingTool.ttslua +++ /dev/null @@ -1,95 +0,0 @@ -local connections = {} - -function updateSave() - self.script_state = JSON.encode({ connections = connections }) -end - -function onLoad(savedData) - if savedData and savedData ~= "" then - local loadedData = JSON.decode(savedData) or {} - connections = loadedData.connections - processLines() - end - - addHotkey("Drawing Tool: Reset", function() - connections = {} - updateSave() - processLines() - end) - addHotkey("Drawing Tool: Redraw", processLines) -end - -function onScriptingButtonDown(index, playerColor) - if index ~= 10 then return end - - Timer.create { - identifier = playerColor .. "_draw_from", - function_name = "draw_from", - parameters = { player = Player[playerColor] }, - delay = 1 - } -end - -function draw_from(params) - local source = params.player.getHoverObject() - if not source then return end - - for _, item in ipairs(params.player.getSelectedObjects()) do - if item ~= source then - if item.getGUID() > source.getGUID() then - addPair(item, source) - else - addPair(source, item) - end - end - end - - processLines() -end - -function onScriptingButtonUp(index, playerColor) - if index ~= 10 then return end - - -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a obj2. - if not Timer.destroy(playerColor .. "_draw_from") then return end - - local items = Player[playerColor].getSelectedObjects() - if #items < 2 then return end - - table.sort(items, function(a, b) return a.getGUID() > b.getGUID() end) - - for i = 1, #items do - local obj1 = items[i] - for j = i, #items do - local obj2 = items[j] - addPair(obj1, obj2) - end - end - - processLines() -end - -function addPair(obj1, obj2) - local guid1 = obj1.getGUID() - local guid2 = obj2.getGUID() - - if not connections[guid1] then connections[guid1] = {} end - connections[guid1][guid2] = not connections[guid1][guid2] - updateSave() -end - -function processLines() - local lines = Global.getVectorLines() or {} - for source_guid, target_guids in pairs(connections) do - local source = getObjectFromGUID(source_guid) - for target_guid, exists in pairs(target_guids) do - if exists then - local target = getObjectFromGUID(target_guid) - if source and target then - table.insert(lines, { points = { source.getPosition(), target.getPosition() }, color = "White" }) - end - end - end - end - Global.setVectorLines(lines) -end