diff --git a/src/core/Global.ttslua b/src/core/Global.ttslua index 08e5268e..0ac0f349 100644 --- a/src/core/Global.ttslua +++ b/src/core/Global.ttslua @@ -34,7 +34,6 @@ local NOT_INTERACTABLE = { local chaosTokens = {} local chaosTokensLastMat = nil -local IS_RESHUFFLING = false local bagSearchers = {} local MAT_COLORS = {"White", "Orange", "Green", "Red"} local hideTitleSplashWaitFunctionId = nil @@ -143,14 +142,6 @@ function onLoad(savedData) math.randomseed(os.time()) 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 -- 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. @@ -179,82 +170,6 @@ function tryObjectEnterContainer(container, object) return true 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 --------------------------------------------------------- diff --git a/src/core/MythosArea.ttslua b/src/core/MythosArea.ttslua index 57455b30..5cac0f66 100644 --- a/src/core/MythosArea.ttslua +++ b/src/core/MythosArea.ttslua @@ -12,9 +12,16 @@ local ENCOUNTER_DISCARD_AREA = { lowerRight = { x = 1.58, z = 0.38 }, } -local currentScenario -local useFrontData -local tokenData +-- global position of encounter deck and discard pile +local ENCOUNTER_DECK_POS = { x = -3.93, y = 1, z = 5.76 } +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_GUID = "70b9f6" @@ -117,6 +124,68 @@ function returnTokenData() } 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, ---@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 @@ -124,16 +193,15 @@ end ---@return Boolean. True if the point is in the area defined by bounds function inArea(point, bounds) return (point.x < bounds.upperLeft.x - and point.x > bounds.lowerRight.x - and point.z < bounds.upperLeft.z - and point.z > bounds.lowerRight.z) + and point.x > bounds.lowerRight.x + and point.z < bounds.upperLeft.z + and point.z > bounds.lowerRight.z) end -- removes tokens from the provided card/deck function removeTokensFromObject(object) for _, v in ipairs(searchArea(object.getPosition(), { 3, 1, 4 })) do local obj = v.hit_object - if obj.getGUID() ~= "4ee1f2" and -- table obj ~= self and obj.type ~= "Deck" and @@ -146,13 +214,31 @@ function removeTokensFromObject(object) end end -function searchArea(origin, size) - return Physics.cast({ +-- searches an area and optionally filters the result +function searchArea(origin, size, filter) + local objList = Physics.cast({ origin = origin, - direction = {0, 1, 0}, + direction = { 0, 1, 0 }, orientation = self.getRotation(), type = 3, size = size, 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 + +-- filter functions for searchArea +function isDeck(x) return x.tag == 'Deck' end + +function isCardOrDeck(x) return x.tag == 'Card' or x.tag == 'Deck' end diff --git a/src/core/MythosAreaApi.ttslua b/src/core/MythosAreaApi.ttslua index a3fcbcf2..4eeeb693 100644 --- a/src/core/MythosAreaApi.ttslua +++ b/src/core/MythosAreaApi.ttslua @@ -7,5 +7,14 @@ do return getObjectFromGUID(MYTHOS_AREA_GUID).call("returnTokenData") 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 end diff --git a/src/playermat/Playmat.ttslua b/src/playermat/Playmat.ttslua index bcc65c47..91b1b94c 100644 --- a/src/playermat/Playmat.ttslua +++ b/src/playermat/Playmat.ttslua @@ -1,6 +1,8 @@ -local tokenManager = require("core/token/TokenManager") -local tokenChecker = require("core/token/TokenChecker") +local chaosBagApi = require("chaosbag/ChaosBagApi") +local mythosAreaApi = require("core/MythosAreaApi") 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() local DEBUG = false @@ -49,17 +51,17 @@ local THREAT_AREA = { } } -local DRAW_DECK_POSITION = { x = -1.82, y = 1, z = 0 } -local DISCARD_PILE_POSITION = { x = -1.82, y = 1.5, z = 0.61 } +-- local position of draw and discard pile +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 -local STAT_TRACKER -local RESOURCE_COUNTER - -local chaosBagApi = require("chaosbag/ChaosBagApi") +-- global position of encounter discard pile +local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38} -- global variable so it can be reset by the Clean Up Helper activeInvestigatorId = "00000" + +local TRASHCAN, STAT_TRACKER, RESOURCE_COUNTER local isDrawButtonVisible = false -- global variable to report "Dream-Enhancing Serum" status @@ -83,11 +85,11 @@ function onLoad(save_state) -- button creation for i = 1, 6 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i) + makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i) end self.createButton({ - click_function = "drawEncountercard", + click_function = "drawEncounterCard", function_owner = self, position = {-1.84, 0, -0.65}, rotation = {0, 80, 0}, @@ -147,18 +149,35 @@ function spawnDeckZone() }) end -function searchArea(origin, size) - return Physics.cast({ +-- searches an area and optionally filters the result +function searchArea(origin, size, filter) + local objList = Physics.cast({ origin = origin, - direction = {0, 1, 0}, + direction = { 0, 1, 0 }, orientation = self.getRotation(), type = 3, size = size, - max_distance = 1, - debug = DEBUG + 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 +-- 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. function searchAroundSelf() local bounds = self.getBoundsNormalized() @@ -170,9 +189,7 @@ function searchAroundSelf() -- table position of the playmat local setAsideDirection = bounds.center.z > 0 and 1 or -1 local localCenter = self.positionToLocal(bounds.center) - localCenter.x = localCenter.x - + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x - + localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x return searchArea(self.positionToWorld(localCenter), bounds.size) end @@ -196,19 +213,17 @@ end -- 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 -function makeDiscardHandlerFor(searchPosition, discardPosition) +function makeDiscardHandlerFor(searchPosition, ) return function () for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do local obj = hitObj.hit_object if obj.tag == "Deck" or obj.tag == "Card" then if obj.hasTag("PlayerCard") then - obj.setPositionSmooth(self.positionToWorld(DISCARD_PILE_POSITION), false, true) - obj.setRotation(self.getRotation()) + placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) else - obj.setPositionSmooth(discardPosition, false, true) - obj.setRotation({0, -90, 0}) + placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, {x = 0, y = -90, z = 0}) end -- put chaos tokens back into bag (e.g. Unrelenting) elseif tokenChecker.isChaosToken(obj) then @@ -222,11 +237,52 @@ function makeDiscardHandlerFor(searchPosition, discardPosition) end end --- build a discard button to discard from searchPosition to discardPosition (number must be unique) -function makeDiscardButton(xValue, discardPosition, number) +-- places a card/deck at a position or merges into an existing deck +-- 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 searchPosition = {-position[1], position[2], position[3] + 0.32} - local handler = makeDiscardHandlerFor(searchPosition, discardPosition) + local handler = makeDiscardHandlerFor(searchPosition) local handlerName = 'handler' .. number self.setVar(handlerName, handler) self.createButton({ @@ -479,7 +535,7 @@ function doDiscardOne() -- get a random non-hidden card (from the "choices" table) 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") end end @@ -782,8 +838,10 @@ function drawChaosTokenButton(_, _, isRightClick) chaosBagApi.drawChaosToken(self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick) end -function drawEncountercard(_, _, isRightClick) - Global.call("drawEncountercard", {self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET), self.getRotation(), isRightClick}) +function drawEncounterCard(_, _, isRightClick) + local pos = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET) + local rotY = self.getRotation().y + mythosAreaApi.drawEncounterCard(pos, rotY, isRightClick) end function returnGlobalDiscardPosition()