diff --git a/src/core/PlayArea.ttslua b/src/core/PlayArea.ttslua index ad422fae..765416f4 100644 --- a/src/core/PlayArea.ttslua +++ b/src/core/PlayArea.ttslua @@ -141,13 +141,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 @@ -187,12 +191,20 @@ 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 + if isInPlayArea(card) then local metadata = JSON.decode(card.getGMNotes()) or { } if metadata.type == "Location" then - locations[card.getGUID()] = metadata - rebuildConnectionList() - drawBaseConnections() + 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 @@ -238,20 +250,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 @@ -259,15 +271,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 @@ -276,25 +287,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 @@ -302,22 +312,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() @@ -552,6 +546,48 @@ 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 + -- 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 069003bb..699ab03a 100644 --- a/src/core/PlayAreaApi.ttslua +++ b/src/core/PlayAreaApi.ttslua @@ -55,6 +55,11 @@ 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 + -- 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) diff --git a/src/core/VictoryDisplay.ttslua b/src/core/VictoryDisplay.ttslua index efa36fad..329ec857 100644 --- a/src/core/VictoryDisplay.ttslua +++ b/src/core/VictoryDisplay.ttslua @@ -1,11 +1,8 @@ local playAreaApi = require("core/PlayAreaApi") local pendingCall = false local messageSent = {} -local victoryPoints = { - display = 0, - playArea = 0 -} +-- button creation when loading the game function onLoad() -- index 0: VP - "Display" local buttonParameters = {} @@ -17,7 +14,7 @@ function onLoad() buttonParameters.height = 0 buttonParameters.font_size = 600 buttonParameters.font_color = { 1, 1, 1 } - buttonParameters.position = { x = -0.71, y = 0.06, z = -0.69 } + buttonParameters.position = { x = -0.72, y = 0.06, z = -0.69 } self.createButton(buttonParameters) -- index 1: VP - "Play Area" @@ -25,63 +22,91 @@ function onLoad() self.createButton(buttonParameters) -- index 2: VP - "Total" - buttonParameters.position.x = 1.685 + buttonParameters.position.x = 1.69 self.createButton(buttonParameters) -- update the display label once Wait.time(updateCount, 1) end --- automatically update when cards are dropped or removed +--------------------------------------------------------- +-- events with descriptions +--------------------------------------------------------- + +-- dropping an object on the victory display function onCollisionEnter() - updateCount() + -- 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() - updateCount() + -- 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 - if obj.memo ~= "clueDoom" then return end - if obj.is_face_down == true and flipped ~= true then return end - if not playAreaApi.isInPlayArea(obj) then return end - delay = tonumber(delay) or 0 - Wait.time(function() updateCount() end, delay) -end -function isClue(obj) - return obj.memo == "clueDoom" and obj.is_face_down == false -end - --- works as a sinkhole for all refresh calls -function updateCount() - if not pendingCall then - pendingCall = true - Wait.time(function() updateCountNow() end, 0.2) + -- 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 -function updateCountNow() +-- counts the VP in the victory display and request the VP count from the play area +function updateCount() + local victoryPoints = {} victoryPoints.display = 0 - victoryPoints.playArea = 0 + victoryPoints.playArea = playAreaApi.countVP() -- count cards in victory display for _, v in ipairs(searchOnObj(self)) do @@ -102,29 +127,16 @@ function updateCountNow() end end - -- count locations in playArea - local playArea = getObjectFromGUID("721ba2") - for _, v in ipairs(searchOnObj(playArea)) do - local obj = v.hit_object - local cardVP = 0 - - if obj.hasTag("Location") then - cardVP = getCardVP(obj.is_face_down, JSON.decode(obj.getGMNotes())) or 0 - if cardVP ~= 0 and not cardHasClues(obj) then - victoryPoints.playArea = victoryPoints.playArea + cardVP - end - end - end - pendingCall = false - updateVP() -end - -function updateVP() + -- 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 +-- sends a message for cards in the victory display that don't have VP function addOrSendMessage(addition, name) if tonumber(addition) ~= nil then return tonumber(addition) @@ -135,17 +147,6 @@ function addOrSendMessage(addition, name) return 0 end --- checks if a card has clues on it -function cardHasClues(card) - for _, v in ipairs(searchOnObj(card)) do - local obj = v.hit_object - if isClue(obj) then - return true - end - end - return false -end - -- gets the VP count from the notes function getCardVP(faceDown, notes) local cardVP @@ -155,6 +156,7 @@ function getCardVP(faceDown, notes) -- location if not cardVP then + -- check the correct side of the location if not faceDown and notes.locationFront ~= nil then cardVP = tonumber(notes.locationFront.victory) elseif notes.locationBack ~= nil then @@ -165,6 +167,11 @@ function getCardVP(faceDown, notes) return cardVP end +--------------------------------------------------------- +-- utility functions +--------------------------------------------------------- + +-- searches on an object function searchOnObj(obj) return Physics.cast({ direction = { 0, 1, 0 },