Merge pull request #377 from argonui/encounter-deck

Encounter card drawing / discard: moved code and fixed TTS merge bug
This commit is contained in:
BootleggerFinn 2023-09-25 15:53:02 -05:00 committed by GitHub
commit 454eb80330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 194 additions and 126 deletions

View File

@ -34,7 +34,6 @@ local NOT_INTERACTABLE = {
local chaosTokens = {} local chaosTokens = {}
local chaosTokensLastMat = nil local chaosTokensLastMat = nil
local IS_RESHUFFLING = false
local bagSearchers = {} local bagSearchers = {}
local MAT_COLORS = {"White", "Orange", "Green", "Red"} local MAT_COLORS = {"White", "Orange", "Green", "Red"}
local hideTitleSplashWaitFunctionId = nil local hideTitleSplashWaitFunctionId = nil
@ -143,14 +142,6 @@ function onLoad(savedData)
math.randomseed(os.time()) math.randomseed(os.time())
end end
---------------------------------------------------------
-- encounter card drawing
---------------------------------------------------------
function isDeck(x) return x.tag == 'Deck' end
function isCardOrDeck(x) return x.tag == 'Card' or x.tag == 'Deck' end
-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag -- Event hook for any object search. When chaos tokens are manipulated while the chaos bag
-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the -- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the
-- chaos bag during search operations to avoid this. -- chaos bag during search operations to avoid this.
@ -179,82 +170,6 @@ function tryObjectEnterContainer(container, object)
return true return true
end end
function drawEncountercard(params)
local position = params[1]
local rotation = params[2]
local alwaysFaceUp = params[3]
local card
local items = findInRadiusBy(ENCOUNTER_DECK_POS, 4, isCardOrDeck)
if #items > 0 then
for _, v in ipairs(items) do
if v.tag == 'Deck' then
card = v.takeObject({index = 0})
break
end
end
-- we didn't find the deck so just pull the first thing we did find
if card == nil then card = items[1] end
actualEncounterCardDraw(card, params)
else
-- nothing here, time to reshuffle
reshuffleEncounterDeck(params)
end
end
function actualEncounterCardDraw(card, params)
local position = params[1]
local rotation = params[2]
local alwaysFaceUp = params[3]
local faceUpRotation = 0
if not alwaysFaceUp then
local metadata = JSON.decode(card.getGMNotes()) or {}
if metadata.hidden or getObjectFromGUID(DATA_HELPER_GUID).call('checkHiddenCard', card.getName()) then
faceUpRotation = 180
end
end
card.setPositionSmooth(position, false, false)
card.setRotationSmooth({0, rotation.y, faceUpRotation}, false, false)
end
function reshuffleEncounterDeck(params)
-- finishes moving the deck back and draws a card
local function move(deck)
deck.setPositionSmooth({ENCOUNTER_DECK_POS[1], ENCOUNTER_DECK_POS[2] + 2, ENCOUNTER_DECK_POS[3]}, false, true)
actualEncounterCardDraw(deck.takeObject({index=0}), params)
Wait.time(function() IS_RESHUFFLING = false end, 1)
end
-- bail out if we're mid reshuffle
if IS_RESHUFFLING then return end
local discarded = findInRadiusBy(ENCOUNTER_DECK_DISCARD_POSITION, 4, isDeck)
if #discarded > 0 then
IS_RESHUFFLING = true
local deck = discarded[1]
if not deck.is_face_down then deck.flip() end
deck.shuffle()
Wait.time(|| move(deck), 0.3)
else
printToAll("Couldn't find encounter discard pile to reshuffle.", {1, 0, 0})
end
end
function findInRadiusBy(pos, radius, filter)
local objList = Physics.cast({
origin = pos,
direction = {0, 1, 0},
type = 2,
size = {radius, radius, radius},
max_distance = 0
})
local filteredList = {}
for _, obj in ipairs(objList) do
if filter and filter(obj.hit_object) then
table.insert(filteredList, obj.hit_object)
end
end
return filteredList
end
--------------------------------------------------------- ---------------------------------------------------------
-- chaos token drawing -- chaos token drawing
--------------------------------------------------------- ---------------------------------------------------------

View File

@ -12,9 +12,16 @@ local ENCOUNTER_DISCARD_AREA = {
lowerRight = { x = 1.58, z = 0.38 }, lowerRight = { x = 1.58, z = 0.38 },
} }
local currentScenario -- global position of encounter deck and discard pile
local useFrontData local ENCOUNTER_DECK_POS = { x = -3.93, y = 1, z = 5.76 }
local tokenData local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1, z = 10.38 }
local isReshuffling = false
-- scenario metadata
local currentScenario, useFrontData, tokenData
-- GUID of data helper
local DATA_HELPER_GUID = "708279"
local TRASHCAN local TRASHCAN
local TRASHCAN_GUID = "70b9f6" local TRASHCAN_GUID = "70b9f6"
@ -117,6 +124,68 @@ function returnTokenData()
} }
end end
---------------------------------------------------------
-- encounter card drawing
---------------------------------------------------------
-- 'params' contains the position, rotation and a boolean to force a faceup draw
function drawEncounterCard(params)
local card
local items = searchArea(ENCOUNTER_DECK_POS, { 3, 1, 4 }, isCardOrDeck)
if #items > 0 then
for _, j in ipairs(items) do
local v = j.hit_object
if v.tag == 'Deck' then
card = v.takeObject({ index = 0 })
break
end
end
-- we didn't find the deck so just pull the first thing we did find
if card == nil then card = items[1].hit_object end
actualEncounterCardDraw(card, params)
else
-- nothing here, time to reshuffle
reshuffleEncounterDeck(params)
end
end
function actualEncounterCardDraw(card, params)
local faceUpRotation = 0
if not params.alwaysFaceUp then
local metadata = JSON.decode(card.getGMNotes()) or {}
if metadata.hidden or getObjectFromGUID(DATA_HELPER_GUID).call('checkHiddenCard', card.getName()) then
faceUpRotation = 180
end
end
card.setPositionSmooth(params.pos, false, false)
card.setRotationSmooth({ 0, params.rotY, faceUpRotation }, false, false)
end
function reshuffleEncounterDeck(params)
-- flag to avoid multiple calls
if isReshuffling then return end
isReshuffling = true
-- shuffle and flip deck, draw card after completion
local discarded = searchArea(ENCOUNTER_DISCARD_POSITION, { 3, 1, 4 }, isDeck)
if #discarded > 0 then
local deck = discarded[1].hit_object
if not deck.is_face_down then deck.flip() end
deck.shuffle()
deck.setPositionSmooth(Vector(ENCOUNTER_DECK_POS) + Vector(0, 2, 0), false, true)
Wait.time(function() actualEncounterCardDraw(deck.takeObject({ index = 0 }), params) end, 0.5)
else
printToAll("Couldn't find encounter discard pile to reshuffle.", { 1, 0, 0 })
end
-- disable flag
Wait.time(function() isReshuffling = false end, 1)
end
---------------------------------------------------------
-- helper functions
---------------------------------------------------------
-- Simple method to check if the given point is in a specified area. Local use only, -- Simple method to check if the given point is in a specified area. Local use only,
---@param point Vector. Point to check, only x and z values are relevant ---@param point Vector. Point to check, only x and z values are relevant
---@param bounds Table. Defined area to see if the point is within. See MAIN_PLAY_AREA for sample ---@param bounds Table. Defined area to see if the point is within. See MAIN_PLAY_AREA for sample
@ -124,16 +193,15 @@ end
---@return Boolean. True if the point is in the area defined by bounds ---@return Boolean. True if the point is in the area defined by bounds
function inArea(point, bounds) function inArea(point, bounds)
return (point.x < bounds.upperLeft.x return (point.x < bounds.upperLeft.x
and point.x > bounds.lowerRight.x and point.x > bounds.lowerRight.x
and point.z < bounds.upperLeft.z and point.z < bounds.upperLeft.z
and point.z > bounds.lowerRight.z) and point.z > bounds.lowerRight.z)
end end
-- removes tokens from the provided card/deck -- removes tokens from the provided card/deck
function removeTokensFromObject(object) function removeTokensFromObject(object)
for _, v in ipairs(searchArea(object.getPosition(), { 3, 1, 4 })) do for _, v in ipairs(searchArea(object.getPosition(), { 3, 1, 4 })) do
local obj = v.hit_object local obj = v.hit_object
if obj.getGUID() ~= "4ee1f2" and -- table if obj.getGUID() ~= "4ee1f2" and -- table
obj ~= self and obj ~= self and
obj.type ~= "Deck" and obj.type ~= "Deck" and
@ -146,13 +214,31 @@ function removeTokensFromObject(object)
end end
end end
function searchArea(origin, size) -- searches an area and optionally filters the result
return Physics.cast({ function searchArea(origin, size, filter)
local objList = Physics.cast({
origin = origin, origin = origin,
direction = {0, 1, 0}, direction = { 0, 1, 0 },
orientation = self.getRotation(), orientation = self.getRotation(),
type = 3, type = 3,
size = size, size = size,
max_distance = 1 max_distance = 1
}) })
if filter then
local filteredList = {}
for _, obj in ipairs(objList) do
if filter(obj.hit_object) then
table.insert(filteredList, obj)
end
end
return filteredList
else
return objList
end
end end
-- filter functions for searchArea
function isDeck(x) return x.tag == 'Deck' end
function isCardOrDeck(x) return x.tag == 'Card' or x.tag == 'Deck' end

View File

@ -7,5 +7,14 @@ do
return getObjectFromGUID(MYTHOS_AREA_GUID).call("returnTokenData") return getObjectFromGUID(MYTHOS_AREA_GUID).call("returnTokenData")
end end
-- draw an encounter card to the requested position/rotation
MythosAreaApi.drawEncounterCard = function(pos, rotY, alwaysFaceUp)
getObjectFromGUID(MYTHOS_AREA_GUID).call("drawEncounterCard", {
pos = pos,
rotY = rotY,
alwaysFaceUp = alwaysFaceUp
})
end
return MythosAreaApi return MythosAreaApi
end end

View File

@ -1,6 +1,8 @@
local tokenManager = require("core/token/TokenManager") local chaosBagApi = require("chaosbag/ChaosBagApi")
local tokenChecker = require("core/token/TokenChecker") local mythosAreaApi = require("core/MythosAreaApi")
local navigationOverlayApi = require("core/NavigationOverlayApi") local navigationOverlayApi = require("core/NavigationOverlayApi")
local tokenChecker = require("core/token/TokenChecker")
local tokenManager = require("core/token/TokenManager")
-- set true to enable debug logging and show Physics.cast() -- set true to enable debug logging and show Physics.cast()
local DEBUG = false local DEBUG = false
@ -49,17 +51,17 @@ local THREAT_AREA = {
} }
} }
local DRAW_DECK_POSITION = { x = -1.82, y = 1, z = 0 } -- local position of draw and discard pile
local DISCARD_PILE_POSITION = { x = -1.82, y = 1.5, z = 0.61 } local DRAW_DECK_POSITION = { x = -1.82, y = 0, z = 0 }
local DISCARD_PILE_POSITION = { x = -1.82, y = 0, z = 0.61 }
local TRASHCAN -- global position of encounter discard pile
local STAT_TRACKER local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38}
local RESOURCE_COUNTER
local chaosBagApi = require("chaosbag/ChaosBagApi")
-- global variable so it can be reset by the Clean Up Helper -- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000" activeInvestigatorId = "00000"
local TRASHCAN, STAT_TRACKER, RESOURCE_COUNTER
local isDrawButtonVisible = false local isDrawButtonVisible = false
-- global variable to report "Dream-Enhancing Serum" status -- global variable to report "Dream-Enhancing Serum" status
@ -83,11 +85,11 @@ function onLoad(save_state)
-- button creation -- button creation
for i = 1, 6 do for i = 1, 6 do
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i) makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i)
end end
self.createButton({ self.createButton({
click_function = "drawEncountercard", click_function = "drawEncounterCard",
function_owner = self, function_owner = self,
position = {-1.84, 0, -0.65}, position = {-1.84, 0, -0.65},
rotation = {0, 80, 0}, rotation = {0, 80, 0},
@ -147,18 +149,35 @@ function spawnDeckZone()
}) })
end end
function searchArea(origin, size) -- searches an area and optionally filters the result
return Physics.cast({ function searchArea(origin, size, filter)
local objList = Physics.cast({
origin = origin, origin = origin,
direction = {0, 1, 0}, direction = { 0, 1, 0 },
orientation = self.getRotation(), orientation = self.getRotation(),
type = 3, type = 3,
size = size, size = size,
max_distance = 1, max_distance = 1
debug = DEBUG
}) })
if filter then
local filteredList = {}
for _, obj in ipairs(objList) do
if filter(obj.hit_object) then
table.insert(filteredList, obj)
end
end
return filteredList
else
return objList
end
end end
-- filter functions for searchArea
function isDeck(x) return x.tag == 'Deck' end
function isCardOrDeck(x) return x.tag == 'Card' or x.tag == 'Deck' end
-- Finds all objects on the playmat and associated set aside zone. -- Finds all objects on the playmat and associated set aside zone.
function searchAroundSelf() function searchAroundSelf()
local bounds = self.getBoundsNormalized() local bounds = self.getBoundsNormalized()
@ -170,9 +189,7 @@ function searchAroundSelf()
-- table position of the playmat -- table position of the playmat
local setAsideDirection = bounds.center.z > 0 and 1 or -1 local setAsideDirection = bounds.center.z > 0 and 1 or -1
local localCenter = self.positionToLocal(bounds.center) local localCenter = self.positionToLocal(bounds.center)
localCenter.x = localCenter.x localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
+ setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x
return searchArea(self.positionToWorld(localCenter), bounds.size) return searchArea(self.positionToWorld(localCenter), bounds.size)
end end
@ -196,19 +213,17 @@ end
-- Discard buttons -- Discard buttons
--------------------------------------------------------- ---------------------------------------------------------
-- builds a function that discards things in searchPosition to discardPosition -- builds a function that discards things in searchPosition
-- stuff on the card/deck will be put into the local trashcan -- stuff on the card/deck will be put into the local trashcan
function makeDiscardHandlerFor(searchPosition, discardPosition) function makeDiscardHandlerFor(searchPosition, )
return function () return function ()
for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do
local obj = hitObj.hit_object local obj = hitObj.hit_object
if obj.tag == "Deck" or obj.tag == "Card" then if obj.tag == "Deck" or obj.tag == "Card" then
if obj.hasTag("PlayerCard") then if obj.hasTag("PlayerCard") then
obj.setPositionSmooth(self.positionToWorld(DISCARD_PILE_POSITION), false, true) placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())
obj.setRotation(self.getRotation())
else else
obj.setPositionSmooth(discardPosition, false, true) placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, {x = 0, y = -90, z = 0})
obj.setRotation({0, -90, 0})
end end
-- put chaos tokens back into bag (e.g. Unrelenting) -- put chaos tokens back into bag (e.g. Unrelenting)
elseif tokenChecker.isChaosToken(obj) then elseif tokenChecker.isChaosToken(obj) then
@ -222,11 +237,52 @@ function makeDiscardHandlerFor(searchPosition, discardPosition)
end end
end end
-- build a discard button to discard from searchPosition to discardPosition (number must be unique) -- places a card/deck at a position or merges into an existing deck
function makeDiscardButton(xValue, discardPosition, number) -- rotation is optional
function placeOrMergeIntoDeck(obj, pos, rot)
if not pos then return end
local offset = 0.5
local deck, card, newPos
-- search the new position for existing card/deck
local searchResult = searchArea(pos, { 1, 1, 1 }, isCardOrDeck)
if #searchResult == 1 then
local match = searchResult[1].hit_object
if match.type == 'Card' then
card = match
elseif match.type == 'Deck' then
deck = match
end
end
-- update vertical component of new position
if card or deck then
local bounds = searchResult[1].hit_object.getBounds()
newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset)
else
newPos = Vector(pos) + Vector(0, offset, 0)
end
-- actual movement of the object
if rot then
obj.setRotationSmooth(rot, false, true)
end
obj.setPositionSmooth(newPos, false, true)
-- this avoids a TTS bug that merges unrelated cards that are not resting
if deck then
Wait.time(function() deck.putObject(obj) end, 0.3)
elseif card then
Wait.time(function() obj.setPosition(newPos) end, 0.3)
end
end
-- build a discard button to discard from searchPosition (number must be unique)
function makeDiscardButton(xValue, number)
local position = { xValue, 0.1, -0.94} local position = { xValue, 0.1, -0.94}
local searchPosition = {-position[1], position[2], position[3] + 0.32} local searchPosition = {-position[1], position[2], position[3] + 0.32}
local handler = makeDiscardHandlerFor(searchPosition, discardPosition) local handler = makeDiscardHandlerFor(searchPosition)
local handlerName = 'handler' .. number local handlerName = 'handler' .. number
self.setVar(handlerName, handler) self.setVar(handlerName, handler)
self.createButton({ self.createButton({
@ -479,7 +535,7 @@ function doDiscardOne()
-- get a random non-hidden card (from the "choices" table) -- get a random non-hidden card (from the "choices" table)
local num = math.random(1, #choices) local num = math.random(1, #choices)
hand[choices[num]].setPosition(returnGlobalDiscardPosition()) placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())
broadcastToAll(playerColor .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White") broadcastToAll(playerColor .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White")
end end
end end
@ -782,8 +838,10 @@ function drawChaosTokenButton(_, _, isRightClick)
chaosBagApi.drawChaosToken(self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick) chaosBagApi.drawChaosToken(self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick)
end end
function drawEncountercard(_, _, isRightClick) function drawEncounterCard(_, _, isRightClick)
Global.call("drawEncountercard", {self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET), self.getRotation(), isRightClick}) local pos = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET)
local rotY = self.getRotation().y
mythosAreaApi.drawEncounterCard(pos, rotY, isRightClick)
end end
function returnGlobalDiscardPosition() function returnGlobalDiscardPosition()