Merge pull request #211 from argonui/victory-display

Victory Display: Visual update and automation
This commit is contained in:
Chr1Z 2023-02-05 03:22:17 +01:00 committed by GitHub
commit 26c0e92e86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 523 additions and 136 deletions

View File

@ -3,7 +3,6 @@
"ComponentTags_path": "ComponentTags.json", "ComponentTags_path": "ComponentTags.json",
"CustomUIAssets_path": "CustomUIAssets.json", "CustomUIAssets_path": "CustomUIAssets.json",
"DecalPallet_path": "DecalPallet.json", "DecalPallet_path": "DecalPallet.json",
"Decals_path": "Decals.json",
"GameComplexity": "", "GameComplexity": "",
"GameMode": "Arkham Horror LCG - Super Complete Edition", "GameMode": "Arkham Horror LCG - Super Complete Edition",
"GameType": "", "GameType": "",
@ -188,7 +187,8 @@
"Decoration-Ammo.0a3b03", "Decoration-Ammo.0a3b03",
"Decoration-Ammo.b43845", "Decoration-Ammo.b43845",
"Decoration-Ammo.d35ee9", "Decoration-Ammo.d35ee9",
"ArkhamSCE300-1272023-Page1.f873a8" "ArkhamSCE300-1272023-Page1.f873a8",
"VictoryDisplay.6ccd6d"
], ],
"PlayArea": 1, "PlayArea": 1,
"PlayerCounts": [ "PlayerCounts": [

View File

@ -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
}
}
]

View File

@ -207,78 +207,6 @@
"z": -32.233 "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": { "Position": {
"x": 65, "x": 65,

View File

@ -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": ""
}

View File

@ -56,6 +56,8 @@ local draggingGuids = {}
local locationData local locationData
local currentScenario local currentScenario
local missingData = {}
--------------------------------------------------------- ---------------------------------------------------------
-- general code -- general code
--------------------------------------------------------- ---------------------------------------------------------
@ -96,7 +98,12 @@ function onCollisionEnter(collisionInfo)
local objType = obj.name local objType = obj.name
-- only continue for cards -- 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 -- check if we should spawn clues here and do so according to playercount
local card = collisionInfo.collision_object 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 if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= "" then
local pickedUpGuid = object.getGUID() local pickedUpGuid = object.getGUID()
local metadata = JSON.decode(object.getGMNotes()) or { } 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 -- 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 -- 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. -- 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 -- Waiting ensures we always do thing in the expected Exit->PickUp order
Wait.frames(function() Wait.frames(function()
draggingGuids[pickedUpGuid] = metadata if object.is_face_down then
draggingGuids[pickedUpGuid] = metadata.locationBack
else
draggingGuids[pickedUpGuid] = metadata.locationFront
end
rebuildConnectionList() rebuildConnectionList()
end, 2) end, 2)
end end
@ -188,15 +199,27 @@ end
function maybeTrackLocation(card) function maybeTrackLocation(card)
-- Collision checks for any part of the card overlap, but our other tracking is centerpoint -- 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 -- 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 { } local metadata = JSON.decode(card.getGMNotes())
if metadata == nil then
table.insert(missingData, card)
else
if metadata.type == "Location" then if metadata.type == "Location" then
locations[card.getGUID()] = metadata 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() rebuildConnectionList()
drawBaseConnections() drawBaseConnections()
end end
end end
end end
end
end
-- Stop tracking a location for connection drawing. This should be called for both collision exit -- 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 -- and destruction, as a destroyed object does not trigger collision exit. An object can also be
@ -239,20 +262,20 @@ function rebuildConnectionList()
-- Build a list of cards with each icon as their location ID -- Build a list of cards with each icon as their location ID
for cardId, metadata in pairs(draggingGuids) do for cardId, metadata in pairs(draggingGuids) do
buildLocListByIcon(cardId, iconCardList) buildLocListByIcon(cardId, iconCardList, metadata)
end end
for cardId, metadata in pairs(locations) do for cardId, metadata in pairs(locations) do
buildLocListByIcon(cardId, iconCardList) buildLocListByIcon(cardId, iconCardList, metadata)
end end
-- Pair up all the icons -- Pair up all the icons
locationConnections = { } locationConnections = { }
for cardId, metadata in pairs(draggingGuids) do for cardId, metadata in pairs(draggingGuids) do
buildConnection(cardId, iconCardList) buildConnection(cardId, iconCardList, metadata)
end end
for cardId, metadata in pairs(locations) do for cardId, metadata in pairs(locations) do
if draggingGuids[cardId] == nil then if draggingGuids[cardId] == nil then
buildConnection(cardId, iconCardList) buildConnection(cardId, iconCardList, metadata)
end end
end end
end end
@ -260,15 +283,14 @@ end
-- Extracts the card's icon string into a list of individual location icons -- 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 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 ---@param iconCardList Table A table of icon->GUID list. Mutable, will be updated by this method
function buildLocListByIcon(cardId, iconCardList) ---@param locData Table A table containing the metadata for the card (for the correct side)
local card = getObjectFromGUID(cardId) function buildLocListByIcon(cardId, iconCardList, locData)
local locData = getLocationData(card)
if locData ~= nil and locData.icons ~= nil then if locData ~= nil and locData.icons ~= nil then
for icon in string.gmatch(locData.icons, "%a+") do for icon in string.gmatch(locData.icons, "%a+") do
if iconCardList[icon] == nil then if iconCardList[icon] == nil then
iconCardList[icon] = { } iconCardList[icon] = { }
end end
table.insert(iconCardList[icon], card.getGUID()) table.insert(iconCardList[icon], cardId)
end end
end end
end end
@ -277,25 +299,24 @@ end
-- Playarea's locationConnections table. -- Playarea's locationConnections table.
---@param cardId String GUID of the card to build the connections for ---@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. ---@param iconCardList Table A table of icon->GUID List. Used to find matching icons for connections.
function buildConnection(cardId, iconCardList) ---@param locData Table A table containing the metadata for the card (for the correct side)
local card = getObjectFromGUID(cardId) function buildConnection(cardId, iconCardList, locData)
local locData = getLocationData(card)
if locData ~= nil and locData.connections ~= nil then if locData ~= nil and locData.connections ~= nil then
locationConnections[card.getGUID()] = { } locationConnections[cardId] = { }
for icon in string.gmatch(locData.connections, "%a+") do for icon in string.gmatch(locData.connections, "%a+") do
if iconCardList[icon] ~= nil then if iconCardList[icon] ~= nil then
for _, connectedGuid in ipairs(iconCardList[icon]) do for _, connectedGuid in ipairs(iconCardList[icon]) do
-- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way -- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way
if locationConnections[connectedGuid] ~= nil if locationConnections[connectedGuid] ~= nil
and locationConnections[connectedGuid][card.getGUID()] == ONE_WAY then and locationConnections[connectedGuid][cardId] == ONE_WAY then
locationConnections[connectedGuid][card.getGUID()] = BIDIRECTIONAL locationConnections[connectedGuid][cardId] = BIDIRECTIONAL
locationConnections[card.getGUID()][connectedGuid] = nil locationConnections[cardId][connectedGuid] = nil
else else
if locationConnections[connectedGuid] == nil then if locationConnections[connectedGuid] == nil then
locationConnections[connectedGuid] = { } locationConnections[connectedGuid] = { }
end end
locationConnections[card.getGUID()][connectedGuid] = ONE_WAY locationConnections[cardId][connectedGuid] = ONE_WAY
locationConnections[connectedGuid][card.getGUID()] = INCOMING_ONE_WAY locationConnections[connectedGuid][cardId] = INCOMING_ONE_WAY
end end
end end
end end
@ -303,22 +324,6 @@ function buildConnection(cardId, iconCardList)
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 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. -- Draws the lines for connections currently in locationConnections but not in draggingGuids.
-- Constructed vectors will be set to the playmat -- Constructed vectors will be set to the playmat
function drawBaseConnections() function drawBaseConnections()
@ -553,6 +558,64 @@ function setLimitSnapsByType(matchTypes)
self.setSnapPoints(snaps) self.setSnapPoints(snaps)
end 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) -- rebuilds local snap points (could be useful in the future again)
function buildSnaps() function buildSnaps()
local upperleft = { x = 1.53, z = -1.09} local upperleft = { x = 1.53, z = -1.09}

View File

@ -55,5 +55,21 @@ do
{ container = container, object = object }) { container = container, object = object })
end 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 return PlayAreaApi
end end

View File

@ -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