diff --git a/src/core/GameKeyHandler.ttslua b/src/core/GameKeyHandler.ttslua index 5fb80afd..a5a8d1f4 100644 --- a/src/core/GameKeyHandler.ttslua +++ b/src/core/GameKeyHandler.ttslua @@ -7,7 +7,9 @@ local victoryDisplayApi = require("core/VictoryDisplayApi") function onLoad() addHotkey("Add Doom to Agenda", addDoomToAgenda) addHotkey("Bless/Curse Status", showBlessCurseStatus) + addHotkey("Discard Object", discardObject) addHotkey("Move card to Victory Display", moveCardToVictoryDisplay) + addHotkey("Remove a use", removeOneUse) addHotkey("Take clue from location", takeClueFromLocation) addHotkey("Upkeep", triggerUpkeep) addHotkey("Upkeep (Multi-handed)", triggerUpkeepMultihanded) @@ -46,11 +48,142 @@ function addDoomToAgenda() doomCounter.call("addVal", 1) end +-- discard the hovered object to the respective trashcan and discard tokens on it if it was a card +function discardObject(playerColor, hoveredObject) + -- only continue if an unlocked card, deck or tile was hovered + if hoveredObject == nil + or (hoveredObject.type ~= "Card" and hoveredObject.type ~= "Deck" and hoveredObject.type ~= "Tile") + or hoveredObject.locked then + broadcastToColor("Hover a token/tile or a card/deck and try again.", playerColor, "Yellow") + return + end + + -- warning for locations since these are usually not meant to be discarded + if hoveredObject.hasTag("Location") then + broadcastToAll("Watch out: A location was discarded.", "Yellow") + end + + -- initialize list of objects to discard + local discardTheseObjects = { hoveredObject } + + -- discard tokens / tiles on cards / decks + if hoveredObject.type ~= "Tile" then + for _, v in ipairs(searchOnObj(hoveredObject)) do + if v.hit_object.type == "Tile" then + table.insert(discardTheseObjects, v.hit_object) + end + end + end + + local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor) + playmatApi.discardListOfObjects(discardForMatColor, discardTheseObjects) +end + +-- helper function to get the player to trigger the discard function for +function getColorToDiscardFor(hoveredObject, playerColor) + local pos = hoveredObject.getPosition() + local closestMatColor = playmatApi.getMatColorByPosition(pos) + + -- check if actually on the closest playmat + local closestMat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, "Playermat") + local bounds = closestMat.getBounds() + + -- define the area "near" the playmat + local bufferAroundPlaymat = 2 + local areaNearPlaymat = {} + areaNearPlaymat.minX = bounds.center.x - bounds.size.x / 2 - bufferAroundPlaymat + areaNearPlaymat.maxX = bounds.center.x + bounds.size.x / 2 + bufferAroundPlaymat + areaNearPlaymat.minZ = bounds.center.z - bounds.size.z / 2 - bufferAroundPlaymat + areaNearPlaymat.maxZ = bounds.center.z + bounds.size.z / 2 + bufferAroundPlaymat + + -- discard to closest mat if near it, use triggering playmat if not + local discardForMatColor + if inArea(pos, areaNearPlaymat) then + return closestMatColor + else + return playmatApi.getMatColor(playerColor) + end +end + -- moves the hovered card to the victory display function moveCardToVictoryDisplay(_, hoveredObject) victoryDisplayApi.placeCard(hoveredObject) end +-- removes a use from a card (or a token if hovered) +function removeOneUse(playerColor, hoveredObject) + -- only continue if an unlocked card or tile was hovered + if hoveredObject == nil + or (hoveredObject.type ~= "Card" and hoveredObject.type ~= "Tile") + or hoveredObject.locked then + broadcastToColor("Hover a token/tile or a card and try again.", playerColor, "Yellow") + return + end + + local targetObject = nil + + -- discard hovered token / tile + if hoveredObject.type == "Tile" then + targetObject = hoveredObject + elseif hoveredObject.type == "Card" then + -- grab the first use type from the metadata (or nil) + local notes = JSON.decode(hoveredObject.getGMNotes()) or {} + local usesData = notes.uses or {} + local useInfo = usesData[1] or {} + local searchForType = useInfo.type + if searchForType then searchForType = searchForType:lower() end + + for _, v in ipairs(searchOnObj(hoveredObject)) do + local obj = v.hit_object + if obj.type == "Tile" and not obj.locked and obj.memo ~= "resourceCounter" then + -- check for matching object, otherwise use the first hit + if obj.memo == searchForType then + targetObject = obj + break + elseif not targetObject then + targetObject = obj + end + end + end + end + + -- error handling + if not targetObject then + broadcastToColor("No tokens found!", playerColor, "Yellow") + return + end + + -- handling for stacked tokens + if targetObject.getQuantity() > 1 then + targetObject = targetObject.takeObject() + end + + -- feedback message + local tokenName = targetObject.getName() + if tokenName == "" then + if targetObject.memo ~= "" then + -- name handling for clue / doom + if targetObject.memo == "clueDoom" then + if targetObject.is_face_down then + tokenName = "Doom" + else + tokenName = "Clue" + end + else + tokenName = titleCase(targetObject.memo) + end + else + tokenName = "Unknown" + end + end + + local playerName = Player[playerColor].steam_name + broadcastToAll(playerName .. " removed a token: " .. tokenName, playerColor) + + local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor) + playmatApi.discardListOfObjects(discardForMatColor, { targetObject }) +end + -- takes a clue from a location, player needs to hover the clue directly or the location function takeClueFromLocation(playerColor, hoveredObject) local cardName, clue @@ -58,7 +191,7 @@ function takeClueFromLocation(playerColor, hoveredObject) if hoveredObject == nil then broadcastToColor("Hover a clue or card with clues and try again.", playerColor, "Yellow") return - elseif hoveredObject.tag == "Card" then + elseif hoveredObject.type == "Card" then cardName = hoveredObject.getName() for _, v in ipairs(searchOnObj(hoveredObject)) do @@ -91,7 +224,7 @@ function takeClueFromLocation(playerColor, hoveredObject) for _, v in ipairs(search) do local obj = v.hit_object - if obj.tag == "Card" then + if obj.type == "Card" then cardName = obj.getName() break end @@ -152,3 +285,20 @@ function searchOnObj(obj) origin = obj.getPosition() }) end + +-- Simple method to check if the given point is in a specified area +---@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 +function inArea(point, bounds) + return (point.x > bounds.minX + and point.x < bounds.maxX + and point.z > bounds.minZ + and point.z < bounds.maxZ) +end + +-- capitalizes the first letter +function titleCase(str) + local first = str:sub(1, 1) + local rest = str:sub(2) + return first:upper() .. rest:lower() +end \ No newline at end of file diff --git a/src/playermat/Playmat.ttslua b/src/playermat/Playmat.ttslua index 5465a9cf..e2061512 100644 --- a/src/playermat/Playmat.ttslua +++ b/src/playermat/Playmat.ttslua @@ -226,26 +226,23 @@ end -- Discard buttons --------------------------------------------------------- --- builds a function that discards things in searchPosition --- stuff on the card/deck will be put into the local trashcan -function makeDiscardHandlerFor(searchPosition) - return function () - local origin = self.positionToWorld(searchPosition) - for _, obj in ipairs(searchArea(origin, {2, 1, 3.2})) do - if isCardOrDeck(obj) then - if obj.hasTag("PlayerCard") then - placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) - else - 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 - local chaosBag = chaosBagApi.findChaosBag() - chaosBag.putObject(obj) - -- don't touch the table or this playmat itself - elseif obj.guid ~= "4ee1f2" and obj ~= self then - ownedObjects.Trash.putObject(obj) +-- handles discarding for a list of objects +---@param objList Table List of objects to discard +function discardListOfObjects(objList) + for _, obj in ipairs(objList) do + if isCardOrDeck(obj) then + if obj.hasTag("PlayerCard") then + placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) + else + 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 + local chaosBag = chaosBagApi.findChaosBag() + chaosBag.putObject(obj) + -- don't touch locked objects (like the table etc.) + elseif not obj.getLock() then + ownedObjects.Trash.putObject(obj) end end end @@ -282,7 +279,7 @@ function placeOrMergeIntoDeck(obj, pos, rot) function() obj.use_hands = true -- this avoids a TTS bug that merges unrelated cards that are not resting - if #searchResult == 1 then + if #searchResult == 1 and searchResult[1] ~= obj then -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put) pcall(function() searchResult[1].putObject(obj) end) end @@ -294,9 +291,13 @@ end 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) local handlerName = 'handler' .. number - self.setVar(handlerName, handler) + self.setVar(handlerName, function() + local cardSizeSearch = {2, 1, 3.2} + local globalSearchPosition = self.positionToWorld(searchPosition) + local searchResult = searchArea(globalSearchPosition, cardSizeSearch) + return discardListOfObjects(searchResult) + end) self.createButton({ label = "Discard", click_function = handlerName, diff --git a/src/playermat/PlaymatApi.ttslua b/src/playermat/PlaymatApi.ttslua index a051cfe6..5105a241 100644 --- a/src/playermat/PlaymatApi.ttslua +++ b/src/playermat/PlaymatApi.ttslua @@ -97,6 +97,15 @@ do end end + -- Handles discarding for the requested playmat for the provided list of objects + ---@param matColor String Color of the playmat - White, Orange, Green or Red (does not support "All") + ---@param objList Table List of objects to discard + PlaymatApi.discardListOfObjects = function(matColor, objList) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("discardListOfObjects", objList) + end + end + -- Returns the active investigator id ---@param matColor String Color of the playmat - White, Orange, Green or Red (does not support "All") PlaymatApi.returnInvestigatorId = function(matColor)