diff --git a/config.json b/config.json index 733a2e22..d832bbf7 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,6 @@ "ComponentTags_path": "ComponentTags.json", "CustomUIAssets_path": "CustomUIAssets.json", "DecalPallet_path": "DecalPallet.json", - "Decals_path": "Decals.json", "GameComplexity": "", "GameMode": "Arkham Horror LCG - Super Complete Edition", "GameType": "", @@ -188,7 +187,8 @@ "Decoration-Ammo.0a3b03", "Decoration-Ammo.b43845", "Decoration-Ammo.d35ee9", - "ArkhamSCE300-1272023-Page1.f873a8" + "ArkhamSCE300-1272023-Page1.f873a8", + "VictoryDisplay.6ccd6d" ], "PlayArea": 1, "PlayerCounts": [ diff --git a/modsettings/Decals.json b/modsettings/Decals.json deleted file mode 100644 index d0bf6815..00000000 --- a/modsettings/Decals.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "CustomDecal": { - "ImageURL": "https://i.imgur.com/saWedQ0.png", - "Name": "Victory Display", - "Size": 15 - }, - "Transform": { - "posX": -1.76003075, - "posY": 1.491499, - "posZ": 28.6174583, - "rotX": 90, - "rotY": 89.6667938, - "rotZ": 0, - "scaleX": 15, - "scaleY": 15, - "scaleZ": 15 - } - } -] diff --git a/modsettings/SnapPoints.json b/modsettings/SnapPoints.json index 6e9d6de8..56935070 100644 --- a/modsettings/SnapPoints.json +++ b/modsettings/SnapPoints.json @@ -207,78 +207,6 @@ "z": -32.233 } }, - { - "Position": { - "x": 0.7, - "y": 1.481, - "z": 33.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, - { - "Position": { - "x": -5.35, - "y": 1.481, - "z": 33.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, - { - "Position": { - "x": -5.35, - "y": 1.481, - "z": 28.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, - { - "Position": { - "x": 0.7, - "y": 1.481, - "z": 28.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, - { - "Position": { - "x": 0.7, - "y": 1.481, - "z": 23.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, - { - "Position": { - "x": -5.35, - "y": 1.481, - "z": 23.6 - }, - "Rotation": { - "x": 0, - "y": 270, - "z": 0 - } - }, { "Position": { "x": 65, diff --git a/objects/VictoryDisplay.6ccd6d.json b/objects/VictoryDisplay.6ccd6d.json new file mode 100644 index 00000000..53e08674 --- /dev/null +++ b/objects/VictoryDisplay.6ccd6d.json @@ -0,0 +1,182 @@ +{ + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "AttachedSnapPoints": [ + { + "Position": { + "x": 0, + "y": 0.05, + "z": 0.05 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": 0.8, + "y": 0.05, + "z": 1 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": 0, + "y": 0.05, + "z": 1 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": 1.6, + "y": 0.05, + "z": 0.05 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": 1.6, + "y": 0.05, + "z": 1 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": -0.8, + "y": 0.05, + "z": 0.05 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": -0.8, + "y": 0.05, + "z": 1 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": -1.6, + "y": 0.05, + "z": 0.05 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": -1.6, + "y": 0.05, + "z": 1 + }, + "Rotation": { + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "Position": { + "x": 0.8, + "y": 0.05, + "z": 0.05 + }, + "Rotation": { + "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": "http://cloud-3.steamusercontent.com/ugc/2053113893674531758/8E71AC0C7F8198A791AB4EA4ED9D301904F9A752/", + "WidthScale": 0 + }, + "Description": "Automatically counts the earned VP from cards in the victory display and locations in the play area without clues on them.", + "DragSelectable": true, + "GMNotes": "", + "GUID": "6ccd6d", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": true, + "LuaScript": "require(\"core/VictoryDisplay\")", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "Custom_Token", + "Nickname": "Victory Display", + "Snap": false, + "Sticky": true, + "Tooltip": true, + "Tags": [ + "CleanUpHelper_ignore" + ], + "Transform": { + "posX": -1.5, + "posY": 1.531, + "posZ": 30, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 4.5, + "scaleY": 1, + "scaleZ": 4.5 + }, + "Value": 0, + "XmlUI": "" +} diff --git a/src/core/PlayArea.ttslua b/src/core/PlayArea.ttslua index bb7d6d97..0151aeb2 100644 --- a/src/core/PlayArea.ttslua +++ b/src/core/PlayArea.ttslua @@ -56,6 +56,8 @@ local draggingGuids = {} local locationData local currentScenario +local missingData = {} + --------------------------------------------------------- -- general code --------------------------------------------------------- @@ -96,7 +98,12 @@ function onCollisionEnter(collisionInfo) local objType = obj.name -- only continue for cards - if not collisionEnabled or (objType ~= "Card" and objType ~= "CardCustom") then return end + if not collisionEnabled or (objType ~= "Card" and objType ~= "CardCustom") then + if objType == "Deck" then + table.insert(missingData, obj) + end + return + end -- check if we should spawn clues here and do so according to playercount local card = collisionInfo.collision_object @@ -142,13 +149,17 @@ function onObjectPickUp(player, object) if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= "" then local pickedUpGuid = object.getGUID() local metadata = JSON.decode(object.getGMNotes()) or { } - if (metadata.type == "Location") then + if metadata.type == "Location" then -- onCollisionExit sometimes comes 1 frame after onObjectPickUp (rather than before it or in -- the same frame). This causes a mismatch in the data between dragging the on-table, and -- that one frame draws connectors on the card which then show up as shadows for snap points. -- Waiting ensures we always do thing in the expected Exit->PickUp order Wait.frames(function() - draggingGuids[pickedUpGuid] = metadata + if object.is_face_down then + draggingGuids[pickedUpGuid] = metadata.locationBack + else + draggingGuids[pickedUpGuid] = metadata.locationFront + end rebuildConnectionList() end, 2) end @@ -188,12 +199,24 @@ end 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() - drawBaseConnections() + if isInPlayArea(card) then + local metadata = JSON.decode(card.getGMNotes()) + if metadata == nil then + table.insert(missingData, card) + else + if metadata.type == "Location" then + if card.is_face_down then + locations[card.getGUID()] = metadata.locationBack + else + locations[card.getGUID()] = metadata.locationFront + end + + -- only draw connection lines for not-excluded scenarios + if showLocationLinks() then + rebuildConnectionList() + drawBaseConnections() + end + end end end end @@ -239,20 +262,20 @@ function rebuildConnectionList() -- Build a list of cards with each icon as their location ID for cardId, metadata in pairs(draggingGuids) do - buildLocListByIcon(cardId, iconCardList) + buildLocListByIcon(cardId, iconCardList, metadata) end for cardId, metadata in pairs(locations) do - buildLocListByIcon(cardId, iconCardList) + buildLocListByIcon(cardId, iconCardList, metadata) end -- Pair up all the icons locationConnections = { } for cardId, metadata in pairs(draggingGuids) do - buildConnection(cardId, iconCardList) + buildConnection(cardId, iconCardList, metadata) end for cardId, metadata in pairs(locations) do if draggingGuids[cardId] == nil then - buildConnection(cardId, iconCardList) + buildConnection(cardId, iconCardList, metadata) end end end @@ -260,15 +283,14 @@ end -- Extracts the card's icon string into a list of individual location icons ---@param cardID String GUID of the card to pull the icon data from ---@param iconCardList Table 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) +---@param locData Table A table containing the metadata for the card (for the correct side) +function buildLocListByIcon(cardId, iconCardList, locData) 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()) + table.insert(iconCardList[icon], cardId) end end end @@ -277,25 +299,24 @@ end -- Playarea's locationConnections table. ---@param cardId String GUID of the card to build the connections for ---@param iconCardList Table 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) +---@param locData Table A table containing the metadata for the card (for the correct side) +function buildConnection(cardId, iconCardList, locData) if locData ~= nil and locData.connections ~= nil then - locationConnections[card.getGUID()] = { } + locationConnections[cardId] = { } 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()] == ONE_WAY then - locationConnections[connectedGuid][card.getGUID()] = BIDIRECTIONAL - locationConnections[card.getGUID()][connectedGuid] = nil + and locationConnections[connectedGuid][cardId] == ONE_WAY then + locationConnections[connectedGuid][cardId] = BIDIRECTIONAL + locationConnections[cardId][connectedGuid] = nil else if locationConnections[connectedGuid] == nil then locationConnections[connectedGuid] = { } end - locationConnections[card.getGUID()][connectedGuid] = ONE_WAY - locationConnections[connectedGuid][card.getGUID()] = INCOMING_ONE_WAY + locationConnections[cardId][connectedGuid] = ONE_WAY + locationConnections[connectedGuid][cardId] = INCOMING_ONE_WAY end end end @@ -303,22 +324,6 @@ function buildConnection(cardId, iconCardList) end end --- Helper method to extract the location metadata from a card based on whether it's front or back --- is showing. ----@param card String 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 but not in draggingGuids. -- Constructed vectors will be set to the playmat function drawBaseConnections() @@ -553,6 +558,64 @@ function setLimitSnapsByType(matchTypes) self.setSnapPoints(snaps) end +-- count victory points on locations in play area +---@return. Returns the total amount of VP found in the play area +function countVP() + local totalVP = 0 + + for cardId, metadata in pairs(locations) do + if metadata ~= nil then + local cardVP = tonumber(metadata.victory) or 0 + if cardVP ~= 0 and not cardHasClues(cardId) then + totalVP = totalVP + cardVP + end + end + end + + return totalVP +end + +-- checks if a card has clues on it, returns true if clues are on it +---@param cardId String GUID of the card to check for clues +function cardHasClues(cardId) + local card = getObjectFromGUID(cardId) + for _, v in ipairs(searchOnObj(card)) do + local obj = v.hit_object + if obj.memo == "clueDoom" and obj.is_face_down == false then + return true + end + end + return false +end + +-- searches on an object (by using its bounds) +---@param obj Object Object to search on +function searchOnObj(obj) + return Physics.cast({ + direction = { 0, 1, 0 }, + max_distance = 0.5, + type = 3, + size = obj.getBounds().size, + origin = obj.getPosition() + }) +end + +-- highlights all locations in the play area without metadata +---@param state Boolean True if highlighting should be enabled +function highlightMissingData(state) + for i, obj in pairs(missingData) do + if obj ~= nil then + if state then + obj.highlightOff("Red") + else + obj.highlightOn("Red") + end + else + missingData[i] = nil + end + end +end + -- rebuilds local snap points (could be useful in the future again) function buildSnaps() local upperleft = { x = 1.53, z = -1.09} diff --git a/src/core/PlayAreaApi.ttslua b/src/core/PlayAreaApi.ttslua index 65ac8487..f762aeeb 100644 --- a/src/core/PlayAreaApi.ttslua +++ b/src/core/PlayAreaApi.ttslua @@ -55,5 +55,21 @@ do { container = container, object = object }) end + -- counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getObjectFromGUID(PLAY_AREA_GUID).call("countVP") + end + + -- highlights all locations in the play area without metadata + ---@param state Boolean True if highlighting should be enabled + PlayAreaApi.highlightMissingData = function(state) + return getObjectFromGUID(PLAY_AREA_GUID).call("highlightMissingData", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getObjectFromGUID(PLAY_AREA_GUID).call("isInPlayArea", object) + end + return PlayAreaApi end diff --git a/src/core/VictoryDisplay.ttslua b/src/core/VictoryDisplay.ttslua new file mode 100644 index 00000000..df076b68 --- /dev/null +++ b/src/core/VictoryDisplay.ttslua @@ -0,0 +1,218 @@ +local playAreaApi = require("core/PlayAreaApi") +local pendingCall = false +local messageSent = {} +local currentlyHighlighting = false +local missingData = {} + +-- button creation when loading the game +function onLoad() + -- index 0: VP - "Display" + local buttonParameters = {} + buttonParameters.label = "0" + buttonParameters.click_function = "none" + buttonParameters.function_owner = self + buttonParameters.scale = { 0.15, 0.15, 0.15 } + buttonParameters.width = 0 + buttonParameters.height = 0 + buttonParameters.font_size = 600 + buttonParameters.font_color = { 1, 1, 1 } + buttonParameters.position = { x = -0.72, y = 0.06, z = -0.69 } + self.createButton(buttonParameters) + + -- index 1: VP - "Play Area" + buttonParameters.position.x = 0.65 + self.createButton(buttonParameters) + + -- index 2: VP - "Total" + buttonParameters.position.x = 1.69 + self.createButton(buttonParameters) + + -- index 3: highlighting button + self.createButton({ + label = "!", + click_function = "highlightMissingData", + tooltip = "Enable highlighting of cards without metadata (VP on these is not counted).", + function_owner = self, + scale = { 0.15, 0.15, 0.15 }, + color = { 1, 0, 0 }, + width = 700, + height = 800, + font_size = 700, + font_color = { 1, 1, 1 }, + position = { x = 1.82, y = 0.06, z = -1.32 } + }) + -- update the display label once + Wait.time(updateCount, 1) +end + +--------------------------------------------------------- +-- events with descriptions +--------------------------------------------------------- + +-- dropping an object on the victory display +function onCollisionEnter() + -- stop if there is already an update call running + if pendingCall then return end + pendingCall = true + Wait.time(updateCount, 0.2) +end + +-- removing an object from the victory display +function onCollisionExit() + -- stop if there is already an update call running + if pendingCall then return end + pendingCall = true + Wait.time(updateCount, 0.2) +end + +-- picking a clue or location up +function onObjectPickUp(_, obj) + maybeUpdate(obj) +end + +-- dropping a clue or location +function onObjectDrop(_, obj) + maybeUpdate(obj, 1) +end + +-- flipping a clue/doom or location +function onObjectRotate(obj, _, flip, _, _, oldFlip) + if flip == oldFlip then return end + maybeUpdate(obj, 1, true) +end + +-- destroying a clue or location +function onObjectDestroy(obj) + maybeUpdate(obj) +end + +--------------------------------------------------------- +-- main functionality +--------------------------------------------------------- + +function maybeUpdate(obj, delay, flipped) + -- stop if there is already an update call running + if pendingCall then return end + + -- stop if obj is nil (by e.g. dropping a clue onto another and making a stack) + if obj == nil then return end + + -- only continue for clues / doom tokens or locations + if obj.hasTag("Location") then + elseif obj.memo == "clueDoom" then + -- only continue if the clue side is up or a doom token is being flipped + if obj.is_face_down == true and flipped ~= true then return end + else + return + end + + -- only continue if the obj in in the play area + if not playAreaApi.isInPlayArea(obj) then return end + + -- set this flag to limit function calls (will be reset by "updateCount") + pendingCall = true + + -- update the count with delay (or 0 if no delay is provided) + -- this is needed to let tokens drop on the card + delay = tonumber(delay) or 0 + Wait.time(updateCount, delay + 0.2) +end + +-- counts the VP in the victory display and request the VP count from the play area +function updateCount() + missingData = {} + local victoryPoints = {} + victoryPoints.display = 0 + victoryPoints.playArea = playAreaApi.countVP() + + -- count cards in victory display + for _, v in ipairs(searchOnObj(self)) do + local obj = v.hit_object + + -- check metadata for VP + if obj.tag == "Card" then + victoryPoints.display = victoryPoints.display + getCardVP(obj, JSON.decode(obj.getGMNotes())) + + -- handling for stacked cards + elseif obj.tag == "Deck" then + for _, deepObj in ipairs(obj.getObjects()) do + victoryPoints.display = victoryPoints.display + getCardVP(obj, JSON.decode(deepObj.gm_notes)) + end + end + end + + -- update the buttons that are used as labels + self.editButton({ index = 0, label = victoryPoints.display }) + self.editButton({ index = 1, label = victoryPoints.playArea }) + self.editButton({ index = 2, label = victoryPoints.display + victoryPoints.playArea }) + + -- allow new update calls + pendingCall = false +end + +-- gets the VP count from the notes +function getCardVP(obj, notes) + local cardVP + if notes ~= nil then + -- enemy, treachery etc. + cardVP = tonumber(notes.victory) + + -- location + if not cardVP then + -- check the correct side of the location + if not obj.is_face_down and notes.locationFront ~= nil then + cardVP = tonumber(notes.locationFront.victory) + elseif notes.locationBack ~= nil then + cardVP = tonumber(notes.locationBack.victory) + end + end + else + table.insert(missingData, obj) + end + return cardVP or 0 +end + +-- toggles the highlight for objects with missing metadata +function highlightMissingData() + self.editButton({ + index = 3, + tooltip = (currentlyHighlighting and "Enable" or "Disable") .. + " highlighting of cards without metadata (VP on these is not counted)." + }) + for _, obj in pairs(missingData) do + if obj ~= nil then + if currentlyHighlighting then + obj.highlightOff("Red") + else + obj.highlightOn("Red") + end + end + end + playAreaApi.highlightMissingData(currentlyHighlighting) + currentlyHighlighting = not currentlyHighlighting +end + +--------------------------------------------------------- +-- utility functions +--------------------------------------------------------- + +-- searches on an object +function searchOnObj(obj) + return Physics.cast({ + direction = { 0, 1, 0 }, + max_distance = 0.5, + type = 3, + size = obj.getBounds().size, + origin = obj.getPosition() + }) +end + +-- search a table for a value, return true if found (else returns false) +function tableContains(table, value) + for _, v in ipairs(table) do + if v == value then + return true + end + end + return false +end