diff --git a/unpacked.ttslua b/unpacked.ttslua index 0c7eee0d8..0c4de4df9 100644 --- a/unpacked.ttslua +++ b/unpacked.ttslua @@ -41,103 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/Global") -end) -__bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local TokenArrangerApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - -- local function to call the token arranger, if it is on the table - ---@param functionName string Name of the function to cal - ---@param argument? table Parameter to pass - local function callIfExistent(functionName, argument) - local tokenArranger = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenArranger") - if tokenArranger ~= nil then - tokenArranger.call(functionName, argument) - end - end - - -- updates the token modifiers with the provided data - ---@param fullData table Contains the chaos token metadata - TokenArrangerApi.onTokenDataChanged = function(fullData) - callIfExistent("onTokenDataChanged", fullData) - end - - -- deletes already laid out tokens - TokenArrangerApi.deleteCopiedTokens = function() - callIfExistent("deleteCopiedTokens") - end - - -- updates the laid out tokens - TokenArrangerApi.layout = function() - Wait.time(function() callIfExistent("layout") end, 0.1) - end - - return TokenArrangerApi -end -end) -__bundle_register("chaosbag/BlessCurseManagerApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local BlessCurseManagerApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getManager() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "BlessCurseManager") - end - - -- removes all taken tokens and resets the counts - BlessCurseManagerApi.removeTakenTokensAndReset = function() - local BlessCurseManager = getManager() - Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Bless") end, 0.05) - Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Curse") end, 0.10) - Wait.time(function() BlessCurseManager.call("doReset", "White") end, 0.15) - end - - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.sealedToken = function(type, guid) - getManager().call("sealedToken", { type = type, guid = guid }) - end - - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.releasedToken = function(type, guid) - getManager().call("releasedToken", { type = type, guid = guid }) - end - - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.returnedToken = function(type, guid) - getManager().call("returnedToken", { type = type, guid = guid }) - end - - -- broadcasts the current status for bless/curse tokens - ---@param playerColor string Color of the player to show the broadcast to - BlessCurseManagerApi.broadcastStatus = function(playerColor) - getManager().call("broadcastStatus", playerColor) - end - - -- removes all bless / curse tokens from the chaos bag and play - ---@param playerColor string Color of the player to show the broadcast to - BlessCurseManagerApi.removeAll = function(playerColor) - getManager().call("doRemove", playerColor) - end - - -- adds bless / curse sealing to the hovered card - ---@param playerColor string Color of the player to show the broadcast to - ---@param hoveredObject tts__Object Hovered object - BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject) - getManager().call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject }) - end - - return BlessCurseManagerApi -end -end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local GUIDReferenceApi = {} @@ -146,6 +49,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -153,21 +57,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -179,16 +83,453 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") + else + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } + end + end + + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end + end + return result + end + + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) + local objList = {} + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) + end + end + return objList + end + + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end + end + + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end + end + + return PlayermatApi +end +end) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } + + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList + end + + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) + end + + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib +end +end) __bundle_register("core/Global", function(require, _LOADED, __bundle_register, __bundle_modules) local blessCurseManagerApi = require("chaosbag/BlessCurseManagerApi") local guidReferenceApi = require("core/GUIDReferenceApi") local mythosAreaApi = require("core/MythosAreaApi") local navigationOverlayApi = require("core/NavigationOverlayApi") local playAreaApi = require("core/PlayAreaApi") -local playmatApi = require("playermat/PlaymatApi") +local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local soundCubeApi = require("core/SoundCubeApi") local tokenArrangerApi = require("accessories/TokenArrangerApi") @@ -225,7 +566,7 @@ local bagSearchers = {} local hideTitleSplashWaitFunctionId = nil -- online functionality related variables -local MOD_VERSION = "3.7.0" +local MOD_VERSION = "3.9.0" local SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main' local library, requestObj, modMeta local acknowledgedUpgradeVersions = {} @@ -239,7 +580,7 @@ local tabIdTable = { tab5 = "fanmadePlayerCards" } --- optionPanel data +-- optionPanel data (intentionally not local!) optionPanel = {} local LANGUAGES = { { code = "zh_CN", name = "简体中文" }, @@ -261,33 +602,33 @@ local RESOURCE_OPTIONS = { --------------------------------------------------------- TOKEN_DATA = { - damage = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/", scale = {0.17, 0.17, 0.17}}, - horror = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/", scale = {0.17, 0.17, 0.17}}, - resource = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/", scale = {0.17, 0.17, 0.17}}, - doom = {image = "https://i.imgur.com/EoL7yaZ.png", scale = {0.17, 0.17, 0.17}}, - clue = {image = "http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/", scale = {0.15, 0.15, 0.15}} + damage = { image = "http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/", scale = { 0.17, 0.17, 0.17 } }, + horror = { image = "http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/", scale = { 0.17, 0.17, 0.17 } }, + resource = { image = "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/", scale = { 0.17, 0.17, 0.17 } }, + doom = { image = "https://i.imgur.com/EoL7yaZ.png", scale = { 0.17, 0.17, 0.17 } }, + clue = { image = "http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/", scale = { 0.15, 0.15, 0.15 } } } ID_URL_MAP = { - ['blue'] = {name = "Elder Sign", url = 'https://i.imgur.com/nEmqjmj.png'}, - ['p1'] = {name = "+1", url = 'https://i.imgur.com/uIx8jbY.png'}, - ['0'] = {name = "0", url = 'https://i.imgur.com/btEtVfd.png'}, - ['m1'] = {name = "-1", url = 'https://i.imgur.com/w3XbrCC.png'}, - ['m2'] = {name = "-2", url = 'https://i.imgur.com/bfTg2hb.png'}, - ['m3'] = {name = "-3", url = 'https://i.imgur.com/yfs8gHq.png'}, - ['m4'] = {name = "-4", url = 'https://i.imgur.com/qrgGQRD.png'}, - ['m5'] = {name = "-5", url = 'https://i.imgur.com/3Ym1IeG.png'}, - ['m6'] = {name = "-6", url = 'https://i.imgur.com/c9qdSzS.png'}, - ['m7'] = {name = "-7", url = 'https://i.imgur.com/4WRD42n.png'}, - ['m8'] = {name = "-8", url = 'https://i.imgur.com/9t3rPTQ.png'}, - ['skull'] = {name = "Skull", url = 'https://i.imgur.com/stbBxtx.png'}, - ['cultist'] = {name = "Cultist", url = 'https://i.imgur.com/VzhJJaH.png'}, - ['tablet'] = {name = "Tablet", url = 'https://i.imgur.com/1plY463.png'}, - ['elder'] = {name = "Elder Thing", url = 'https://i.imgur.com/ttnspKt.png'}, - ['red'] = {name = "Auto-fail", url = 'https://i.imgur.com/lns4fhz.png'}, - ['bless'] = {name = "Bless", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/'}, - ['curse'] = {name = "Curse", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/'}, - ['frost'] = {name = "Frost", url = 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/'} + ['blue'] = { name = "Elder Sign", url = 'https://i.imgur.com/nEmqjmj.png' }, + ['p1'] = { name = "+1", url = 'https://i.imgur.com/uIx8jbY.png' }, + ['0'] = { name = "0", url = 'https://i.imgur.com/btEtVfd.png' }, + ['m1'] = { name = "-1", url = 'https://i.imgur.com/w3XbrCC.png' }, + ['m2'] = { name = "-2", url = 'https://i.imgur.com/bfTg2hb.png' }, + ['m3'] = { name = "-3", url = 'https://i.imgur.com/yfs8gHq.png' }, + ['m4'] = { name = "-4", url = 'https://i.imgur.com/qrgGQRD.png' }, + ['m5'] = { name = "-5", url = 'https://i.imgur.com/3Ym1IeG.png' }, + ['m6'] = { name = "-6", url = 'https://i.imgur.com/c9qdSzS.png' }, + ['m7'] = { name = "-7", url = 'https://i.imgur.com/4WRD42n.png' }, + ['m8'] = { name = "-8", url = 'https://i.imgur.com/9t3rPTQ.png' }, + ['skull'] = { name = "Skull", url = 'https://i.imgur.com/stbBxtx.png' }, + ['cultist'] = { name = "Cultist", url = 'https://i.imgur.com/VzhJJaH.png' }, + ['tablet'] = { name = "Tablet", url = 'https://i.imgur.com/1plY463.png' }, + ['elder'] = { name = "Elder Thing", url = 'https://i.imgur.com/ttnspKt.png' }, + ['red'] = { name = "Auto-fail", url = 'https://i.imgur.com/lns4fhz.png' }, + ['bless'] = { name = "Bless", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/' }, + ['curse'] = { name = "Curse", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/' }, + ['frost'] = { name = "Frost", url = 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/' } } --------------------------------------------------------- @@ -312,11 +653,13 @@ function onSave() end function onLoad(savedData) - if savedData then - loadedData = JSON.decode(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) optionPanel = loadedData.optionPanel acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions + chaosTokensLastMatGUID = loadedData.chaosTokensLastMatGUID + -- hack to set options on load optionPanel["useResourceCounters"] = "enabled" updateOptionPanelState() @@ -325,9 +668,8 @@ function onLoad(savedData) for _, guid in ipairs(loadedData.chaosTokensGUID or {}) do table.insert(chaosTokens, getObjectFromGUID(guid)) end - chaosTokensLastMatGUID = loadedData.chaosTokensLastMatGUID - else - print("Saved state could not be found!") + + updateOptionPanelState() end for _, guid in ipairs(NOT_INTERACTABLE) do @@ -344,8 +686,13 @@ function onLoad(savedData) end, 1) 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 +-- provides a random seed (from 1 to 999) to be used by "linked" objects like the action tokens +function getRandomSeed() + return math.random(999) +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. function onObjectSearchStart(object, playerColor) local chaosBag = findChaosBag() @@ -354,51 +701,119 @@ function onObjectSearchStart(object, playerColor) end 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 +-- 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. function onObjectSearchEnd(object, playerColor) local chaosBag = findChaosBag() if object == chaosBag then bagSearchers[playerColor] = nil end + Player[playerColor].clearSelectedObjects() end -- Pass object enter container events to the PlayArea to clear vector lines from dragged cards. -- This requires the try method as cards won't exist any more after they enter a deck, so the lines -- can't be cleared. function tryObjectEnterContainer(container, object) + -- stop mini cards from forming decks + if object.hasTag("Minicard") and container.hasTag("Minicard") then + return false + end + playAreaApi.tryObjectEnterContainer(container, object) return true end -- TTS event for objects that enter zones --- used to detect the "token discard zones" beneath the hand zones -function onObjectEnterZone(zone, enteringObj) - if zone.getName() ~= "TokenDiscardZone" then return end - if tokenChecker.isChaosToken(enteringObj) then return end - - if enteringObj.type == "Tile" and enteringObj.getMemo() and enteringObj.getLock() == false then - local matcolor = playmatApi.getMatColorByPosition(enteringObj.getPosition()) +function onObjectEnterZone(zone, object) + -- detect the "token discard zones" beneath the hand zones + if zone.getName() == "TokenDiscardZone" and + not tokenChecker.isChaosToken(object) and + object.type == "Tile" and + object.getMemo() and + not object.getLock() then + local matcolor = playermatApi.getMatColorByPosition(object.getPosition()) local trash = guidReferenceApi.getObjectByOwnerAndType(matcolor, "Trash") - trash.putObject(enteringObj) + trash.putObject(object) + elseif zone.type == "Hand" and object.type == "Card" then + -- make sure the card is face-up + if object.is_face_down then + object.flip() + end + + -- disable any helpers on the card + if object.hasTag("CardWithHelper") then + object.call("setHelperState", false) + end + + -- maybe reset data about sealed tokens (if that function exists) + if object.hasTag("CardThatSeals") then + local func = object.getVar("resetSealedTokens") + if func ~= nil then + object.call("resetSealedTokens") + end + end end end +-- TTS event for objects that leave zones +function onObjectLeaveZone(zone, object) + -- 1 frame delay to avoid error messages when exiting the game + Wait.frames( + function() + -- end here if one of the objects doesn't exist + if zone.isDestroyed() or object.isDestroyed() then return end + + -- resync the state of the helper on the card with the option panel + if zone.type == "Hand" and object.hasTag("CardWithHelper") then + object.call("syncDisplayWithOptionPanel") + end + end, 1) +end + -- handle card drawing via number typing for multihanded gameplay -- (and additionally allow Norman Withers to draw multiple cards via number) function onObjectNumberTyped(hoveredObject, playerColor, number) -- only continue for decks or cards if hoveredObject.type ~= "Deck" and hoveredObject.type ~= "Card" then return end - - -- check whether the hovered object is part of a players draw objects - for _, color in ipairs(playmatApi.getUsedMatColors()) do - local deckAreaObjects = playmatApi.getDeckAreaObjects(color) - if deckAreaObjects.topCard == hoveredObject or deckAreaObjects.draw == hoveredObject then - playmatApi.drawCardsWithReshuffle(color, number) + + -- check if this is a card with states (and then change state instead of drawing it) + local states = hoveredObject.getStates() + if states ~= nil and #states > 0 then + local stateId = hoveredObject.getStateId() + if stateId ~= number and (#states + 1) >= number then + hoveredObject.setState(number) return true end end + + -- check whether the hovered object is part of a players draw objects + for _, color in ipairs(playermatApi.getUsedMatColors()) do + local deckAreaObjects = playermatApi.getDeckAreaObjects(color) + if deckAreaObjects.topCard == hoveredObject or deckAreaObjects.draw == hoveredObject then + playermatApi.drawCardsWithReshuffle(color, number) + return true + end + end +end + +-- TTS event, used to redraw the playermat slot symbols after a small delay to account for the custom font loading +function onPlayerConnect() + Wait.time(function() playermatApi.redrawSlotSymbols("All") end, 0.2) +end + +-- disable delete action (only applies to promoted players) and discard objects instead +function onPlayerAction(player, action, targets) + if action == Player.Action.Delete and not player.admin then + for _, target in ipairs(targets) do + local matColor = playermatApi.getMatColorByPosition(target.getPosition()) + local trash = guidReferenceApi.getObjectByOwnerAndType(matColor, "Trash") + trash.putObject(target) + end + return false + end + return true end --------------------------------------------------------- @@ -425,33 +840,174 @@ function findChaosBag() printToAll("Chaos bag couldn't be found.", "Red") end +-- returns all chaos tokens to the bag function returnChaosTokens() local chaosBag = findChaosBag() for _, token in pairs(chaosTokens) do if token ~= nil then chaosBag.putObject(token) end end chaosTokens = {} + isTokenXMLActive = false end -- returns a single chaos token to the bag and calls respective functions -function returnChaosTokenToBag(token) - local name = token.getName() - local guid = token.getGUID() +function returnChaosTokenToBag(params) + local name = params.token.getName() local chaosBag = findChaosBag() - chaosBag.putObject(token) + chaosBag.putObject(params.token) tokenArrangerApi.layout() if name == "Bless" or name == "Curse" then - blessCurseManagerApi.releasedToken(name, guid) + blessCurseManagerApi.releasedToken(name, params.token.getGUID(), params.fromBag) end end --- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens --- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the +-- returns the index of a token in the chaosTokens table +function getTokenIndex(token) + for i, obj in ipairs(chaosTokens) do + if obj == token then + return i + end + end +end + +-- starts a redraw effect and displays buttons for a choice if needed +function activeRedrawEffect(params) + redrawData = params + + if isTokenXMLActive == true then + broadcastToAll("Clear already active buttons first, then try again", "Red") + return + end + + if #chaosTokens == 0 then + broadcastToAll("No tokens found in play area", "Red") + return + end + + -- nil handling + redrawData.VALID_TOKENS = redrawData.VALID_TOKENS or {} + redrawData.INVALID_TOKENS = redrawData.INVALID_TOKENS or {} + + -- determine if only some tokens are able to be returned to the bag + local matchingTokensInPlay = {} + for _, token in ipairs(chaosTokens) do + local tokenName = getReadableTokenName(token.getName()) + + -- allow valid tokens or not invalid tokens, also allow any token if both lists empty + if (redrawData.VALID_TOKENS[tokenName] ~= nil and isTableEmpty(redrawData.INVALID_TOKENS)) or + (isTableEmpty(redrawData.VALID_TOKENS) and not redrawData.INVALID_TOKENS[tokenName]) or + (isTableEmpty(redrawData.VALID_TOKENS) and isTableEmpty(redrawData.INVALID_TOKENS)) then + table.insert(matchingTokensInPlay, token) + end + end + + -- proceed according to number of matching tokens + if #matchingTokensInPlay == 0 then + broadcastToAll("No eligible token found in play area", "Red") + elseif #matchingTokensInPlay == 1 then + returnAndRedraw(_, matchingTokensInPlay[1].getGUID()) + else + -- draw XML to allow choosing the token to return to bag + isTokenXMLActive = true + for _, token in ipairs(matchingTokensInPlay) do + token.UI.setXmlTable({ + { + tag = "VerticalLayout", + attributes = { + height = 275, + width = 275, + padding = "0 0 20 25", + scale = "0.4 0.4 1", + rotation = "0 0 180", + position = "0 0 -15", + color = "rgba(0,0,0,0.7)", + onClick = "Global/returnAndRedraw(" .. token.getGUID() .. ")", + }, + children = { + { + tag = "Text", + attributes = { + fontSize = "100", + font = "font_teutonic-arkham", + color = "#ffffff", + text = "Redraw" + } + }, + { + tag = "Text", + attributes = { + fontSize = "125", + font = "font_arkhamicons", + color = "#ffffff", + text = "u" + } + } + } + } + }) + end + end +end + +-- returns a chaos token to the chaos bag and redraws another +function returnAndRedraw(_, tokenGUID) + local returnedToken = getObjectFromGUID(tokenGUID) + local tokenName = returnedToken.getName() + local indexOfReturnedToken = getTokenIndex(returnedToken) + local matColor = playermatApi.getMatColorByPosition(returnedToken.getPosition()) + local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") + + local takeParameters = { + position = returnedToken.getPosition(), + rotation = returnedToken.getRotation() + } + + if #chaosTokens > indexOfReturnedToken then + takeParameters.rotation = takeParameters.rotation + Vector(0, 0, -8) + end + + -- perform the actual token replacing + trackChaosToken(tokenName, mat.getGUID(), true) + local params = {token = returnedToken, fromBag = true} + returnChaosTokenToBag(params) + + chaosTokens[indexOfReturnedToken] = drawChaosToken({ + mat = mat, + drawAdditional = true, + tokenType = redrawData.DRAW_SPECIFIC_TOKEN, -- currently only used for Nkosi Mabati + takeParameters = takeParameters + }) + + -- remove these tokens from the bag + if redrawData.RETURN_TO_POOL then + -- let the bless/curse manager handle these + if tokenName == "Bless" or tokenName == "Curse" then + blessCurseManagerApi.removeToken(tokenName) + else + local invertedTable = createChaosTokenNameLookupTable() + removeChaosToken(invertedTable[tokenName]) + end + end + + -- remove XML from tokens in play + isTokenXMLActive = false + for _, token in ipairs(chaosTokens) do + token.UI.setXml("") + end + + redrawData = {} + + -- return a reference to the freshly drawn token + return chaosTokens[indexOfReturnedToken] +end + +-- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens +-- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the -- contents of the bag should check this method before doing so. -- This method will broadcast a message to all players if the bag is being searched. ---@return boolean: True if the bag is manipulated, false if it should be blocked. function canTouchChaosTokens() - for color, searching in pairs(bagSearchers) do + for _, searching in pairs(bagSearchers) do if searching then broadcastToAll("Someone is searching the chaos bag, can't touch the tokens.", "Red") return false @@ -460,14 +1016,29 @@ function canTouchChaosTokens() return true end +-- converts the human readable name to the empty name that the bag uses +function getChaosTokenName(tokenName) + if tokenName == "Custom Token" then + tokenName = "" + end + return tokenName +end + +-- converts the empty name to the human readable name +function getReadableTokenName(tokenName) + if tokenName == "" then + tokenName = "Custom Token" + end + return tokenName +end + -- called by playermats (by the "Draw chaos token" button) function drawChaosToken(params) if not canTouchChaosTokens() then return end - local tokenOffset = {-1.55, 0.25, -0.58} local matGUID = params.mat.getGUID() - -- return token(s) on other playmat first + -- return token(s) on other playermat first if chaosTokensLastMatGUID ~= nil and chaosTokensLastMatGUID ~= matGUID and #chaosTokens ~= 0 then returnChaosTokens() chaosTokensLastMatGUID = nil @@ -483,44 +1054,45 @@ function drawChaosToken(params) chaosBag.shuffle() -- add the token to the list, compute new position based on list length - tokenOffset[1] = tokenOffset[1] + (0.17 * #chaosTokens) - + local tokenOffset = Vector(-1.55 + 0.17 * #chaosTokens, 0.25, -0.58) + local takeParameters = params.takeParameters or {} + takeParameters.position = takeParameters.position or params.mat.positionToWorld(tokenOffset) + takeParameters.rotation = takeParameters.rotation or params.mat.getRotation() + local token - - if params.guidToBeResolved then -- resolve a sealed token from a card + if params.guidToBeResolved then + -- resolve a sealed token from a card token = getObjectFromGUID(params.guidToBeResolved) - token.setPositionSmooth(params.mat.positionToWorld(tokenOffset)) - local guid = token.getGUID() - local tokenType = token.getName() - if tokenType == "Bless" or tokenType == "Curse" then - blessCurseManagerApi.releasedToken(tokenType, guid) - end + token.setPositionSmooth(takeParameters.position) tokenArrangerApi.layout() - else -- take a token from the bag, either specified or random - local takeParameters = { - position = params.mat.positionToWorld(tokenOffset), - rotation = params.mat.getRotation() - } - if params.tokenType then - for i, lookedForToken in ipairs(chaosBag.getObjects()) do - if lookedForToken.name == params.tokenType then - takeParameters.index = i - 1 - end - end + local tokenName = token.getName() + if tokenName == "Bless" or tokenName == "Curse" then + blessCurseManagerApi.releasedToken(tokenName, token.getGUID()) + end + else + -- take a token from the bag, either specified or random + if params.tokenType then + for i, lookedForToken in ipairs(chaosBag.getObjects()) do + if lookedForToken.nickname == params.tokenType then + takeParameters.index = i - 1 + end + end end - token = chaosBag.takeObject(takeParameters) - end + end + -- get data for token description local name = token.getName() local tokenData = mythosAreaApi.returnTokenData().tokenData or {} local specificData = tokenData[name] or {} token.setDescription(specificData.description or "") - - -- track the chaos token (for stat tracker and future returning) trackChaosToken(name, matGUID) - chaosTokens[#chaosTokens + 1] = token + + if not params.takeParameters then + table.insert(chaosTokens, token) + end + return token else returnChaosTokens() end @@ -530,9 +1102,9 @@ end -- token spawning --------------------------------------------------------- --- DEPRECATED. Use TokenManager instead. +-- DEPRECATED. Use TokenManager instead. -- Spawns a single token. ----@param params table Array with arguments to the method. 1 = position, 2 = type, 3 = rotation +---@param params table Array with arguments to the method. 1 = position, 2 = type, 3 = rotation function spawnToken(params) return tokenManager.spawnToken(params[1], params[2], params[3]) end @@ -541,13 +1113,15 @@ end -- chaos token stat tracker --------------------------------------------------------- -function trackChaosToken(tokenName, matGUID) +function trackChaosToken(tokenName, matGUID, subtract) -- initialize tables if not tokenDrawingStats[matGUID] then tokenDrawingStats[matGUID] = {} end - -- increase stats by 1 - tokenDrawingStats["Overall"][tokenName] = (tokenDrawingStats["Overall"][tokenName] or 0) + 1 - tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + 1 + -- increase stats by 1 (or decrease if token is returned) + local modifier = (subtract and -1 or 1) + tokenName = getReadableTokenName(tokenName) + tokenDrawingStats["Overall"][tokenName] = (tokenDrawingStats["Overall"][tokenName] or 0) + modifier + tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + modifier end -- Left-click: print stats, Right-click: reset stats @@ -566,9 +1140,8 @@ function handleStatTrackerClick(_, _, isRightClick) playerColor = "White" playerName = "Overall" else - -- get mat color - local matColor = playmatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition()) - playerColor = playmatApi.getPlayerColor(matColor) + local matColor = playermatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition()) + playerColor = playermatApi.getPlayerColor(matColor) playerName = Player[playerColor].steam_name or playerColor local playerSquidCount = personalStats["Auto-fail"] or 0 @@ -580,7 +1153,7 @@ function handleStatTrackerClick(_, _, isRightClick) -- get the total count of drawn tokens for the player local totalCount = 0 - for tokenName, value in pairs(personalStats) do + for _, value in pairs(personalStats) do totalCount = totalCount + value end @@ -590,11 +1163,22 @@ function handleStatTrackerClick(_, _, isRightClick) printToAll("------------------------------") printToAll(playerName .. " Stats", playerColor) - for tokenName, value in pairs(personalStats) do - if value ~= 0 then + -- print stats in order of the "ID_URL_MAP" + for _, subtable in pairs(ID_URL_MAP) do + local tokenName = subtable.name + local value = personalStats[tokenName] + if value and value ~= 0 then printToAll(tokenName .. ': ' .. tostring(value)) end end + + -- also print stats for custom tokens + local customTokenName = getReadableTokenName("") + local customTokenCount = personalStats[customTokenName] + if customTokenCount and customTokenCount ~= 0 then + printToAll(customTokenName .. ': ' .. tostring(customTokenCount)) + end + printToAll('Total: ' .. tostring(totalCount)) end end @@ -750,7 +1334,7 @@ function getChaosTokensinPlay() return chaosTokens end --- returns a Table List of chaos token ids in the current chaos bag +-- returns a table of chaos token ids in the current chaos bag ---@api ChaosBag / ChaosBagApi function getChaosBagState() local tokens = {} @@ -868,19 +1452,8 @@ function removeChaosToken(id) printToAll("Removing " .. name .. " token (in bag: " .. #tokens - 1 .. ")", "White") end --- empty the chaos bag -function emptyChaosBag() - if not canTouchChaosTokens() then return end - - local chaosBag = findChaosBag() - for _, object in ipairs(chaosBag.getObjects()) do - chaosBag.takeObject({ callback_function = function(item) item.destruct() end }) - end -end - -- returns all sealed tokens on cards to the chaos bag function releaseAllSealedTokens(playerColor) - local chaosBag = findChaosBag() for _, obj in ipairs(getObjectsWithTag("CardThatSeals")) do obj.call("releaseAllTokens", playerColor) end @@ -910,7 +1483,7 @@ end function onClick_select(_, _, identificationKey) UI.setAttribute("panel" .. currentListItem, "color", "clear") UI.setAttribute(contentToShow .. "_" .. currentListItem, "color", "white") - + -- parses the identification key (contentToShow_currentListItem) if identificationKey then contentToShow = nil @@ -972,12 +1545,14 @@ function placeholder_download(params) UI.setAttribute('download_progress', 'active', false) -- hide download window - changeWindowVisibilityForColor(params.player.color, "downloadWindow", false) + if params.player then + changeWindowVisibilityForColor(params.player.color, "downloadWindow", false) + end return 1 end local url = SOURCE_REPO .. '/' .. params.url - requestObj = WebRequest.get(url, function (request) contentDownloadCallback(request, params) end) + requestObj = WebRequest.get(url, function(request) contentDownloadCallback(request, params) end) startLuaCoroutine(Global, 'downloadCoroutine') end @@ -1029,7 +1604,7 @@ function coroutineDownloadAll() contained = contained .. request.text .. "," downloadedItems = downloadedItems + 1 break - -- time-out if item can't be loaded in 5s + -- time-out if item can't be loaded in 5s elseif request.is_error or (os.time() - start) > 5 then skippedItems = skippedItems + 1 break @@ -1041,7 +1616,7 @@ function coroutineDownloadAll() JSONCopy = JSONCopy .. contained .. "]}" JSONCopy = JSONCopy:gsub("{{POSX}}", posx) JSONCopy = JSONCopy:gsub("{{NICKNAME}}", contentType) - spawnObjectJSON({json = JSONCopy}) + spawnObjectJSON({ json = JSONCopy }) posx = posx + 3 end @@ -1063,7 +1638,7 @@ function onClick_spawnPlaceholder(player) end -- get data for placeholder - local spawnPos = {-39.5, 2, -87} + local spawnPos = { -39.5, 2, -87 } local meshTable = { big = "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj", @@ -1072,15 +1647,15 @@ function onClick_spawnPlaceholder(player) } local scaleTable = { - big = {1.00, 0.14, 1.00}, - small = {2.21, 0.46, 2.42}, - wide = {2.00, 0.11, 1.69} + big = { 1.00, 0.14, 1.00 }, + small = { 2.21, 0.46, 2.42 }, + wide = { 2.00, 0.11, 1.69 } } local placeholder = spawnObject({ type = "Custom_Model", position = spawnPos, - rotation = {0, 270, 0}, + rotation = { 0, 270, 0 }, scale = scaleTable[item.boxsize], }) @@ -1094,7 +1669,7 @@ function onClick_spawnPlaceholder(player) placeholder.addTag("LargeBox") end - placeholder.setColorTint({1, 1, 1, 71/255}) + placeholder.setColorTint({ 1, 1, 1, 71 / 255 }) placeholder.setName(item.name) placeholder.setDescription("by " .. (item.author or "Unknown")) placeholder.setGMNotes(item.url) @@ -1117,7 +1692,7 @@ function onClick_toggleUi(player, windowId) -- hide the playAreaGallery if visible if windowId == "downloadWindow" then changeWindowVisibilityForColor(player.color, "playAreaGallery", false) - -- hide the downloadWindow if visible + -- hide the downloadWindow if visible elseif windowId == "playAreaGallery" then changeWindowVisibilityForColor(player.color, "downloadWindow", false) end @@ -1185,7 +1760,8 @@ end -- updates the preview window function updatePreviewWindow() local item = library[contentToShow][currentListItem] - local tempImage = "http://cloud-3.steamusercontent.com/ugc/2115061845788345842/2CD6ABC551555CCF58F9D0DDB7620197BA398B06/" + local tempImage = + "http://cloud-3.steamusercontent.com/ugc/2115061845788345842/2CD6ABC551555CCF58F9D0DDB7620197BA398B06/" -- set default image if not defined if item.boxsize == nil or item.boxsize == "" or item.boxart == nil or item.boxart == "" then @@ -1224,7 +1800,7 @@ function updatePreviewWindow() -- loading empty image as placeholder until real image is loaded UI.setAttribute("previewArtImage", "image", tempImage) - + -- insert the image itself UI.setAttribute("previewArtImage", "image", item.boxart) UI.setAttributes("previewArtMask", maskData) @@ -1341,13 +1917,14 @@ function contentDownloadCallback(request, params) -- if position is undefined, get empty position if not spawnTable.position then - spawnTable.rotation = { 0, 270, 0} + spawnTable.rotation = { 0, 270, 0 } local pos = getValidSpawnPosition() if pos then spawnTable.position = pos else - broadcastToAll("Please make space in the area below the tentacle stand in the upper middle of the table and try again.", "Red") + broadcastToAll( + "Please make space in the area below the tentacle stand in the upper middle of the table and try again.", "Red") return end end @@ -1365,7 +1942,7 @@ function contentDownloadCallback(request, params) distance = 65 }) end - + -- ping object local pingPlayer = params.player or Player.getPlayers()[1] pingPlayer.pingTable(obj.getPosition()) @@ -1418,7 +1995,7 @@ function libraryDownloadCallback(request) end local json_response = nil - if pcall(function () json_response = JSON.decode(request.text) end) then + if pcall(function() json_response = JSON.decode(request.text) end) then formatLibrary(json_response) updateDownloadItemList() else @@ -1444,19 +2021,12 @@ end -- Option Panel related functionality --------------------------------------------------------- --- called by toggling an option -function onClick_toggleOption(_, id) - local state = self.UI.getAttribute(id, "isOn") - - -- flip state (and handle stupid "False" value) - if state == "False" then - state = true - else - state = false - end - - self.UI.setAttribute(id, "isOn", state) - applyOptionPanelChange(id, state) +-- changes the UI state and the internal variable for the togglebuttons +function onClick_toggleOption(_, _, id) + local currentState = optionPanel[id] + local newState = not currentState + applyOptionPanelChange(id, newState) + UI.setAttribute(id, "image", newState and "option_on" or "option_off") end -- color selection for playArea @@ -1505,7 +2075,11 @@ function playermatRemovalSelected(player, selectedIndex, id) if mat then -- confirmation dialog about deletion player.pingTable(mat.getPosition()) - player.showConfirmDialog("Do you really want to remove " .. matColor .. "'s playermat and related objects? This can't be reversed.", function() removePlayermat(matColor) end) + player.showConfirmDialog( + "Do you really want to remove " .. matColor .. "'s playermat and related objects? This can't be reversed.", + function() + removePlayermat(matColor) + end) else -- info dialog that it is already deleted player.showInfoDialog(matColor .. "'s playermat has already been removed.") @@ -1522,7 +2096,7 @@ function removePlayermat(matColor) if not matObjects.Playermat then return end -- remove action tokens - local actionTokens = playmatApi.searchAroundPlaymat(matColor, "isActionToken") + local actionTokens = playermatApi.searchAroundPlayermat(matColor, "isUniversalToken") for _, obj in ipairs(actionTokens) do obj.destruct() end @@ -1547,85 +2121,79 @@ function updateOptionPanelState() elseif (type(optionValue) == "boolean" and optionValue) or (type(optionValue) == "string" and optionValue) or (type(optionValue) == "table" and #optionValue ~= 0) then - UI.setAttribute(id, "isOn", true) + UI.setAttribute(id, "image", "option_on") else - UI.setAttribute(id, "isOn", "False") + UI.setAttribute(id, "image", "option_off") end end end --- handles the applying of option selections and calls the respective functions based +-- handles the applying of option selections and calls the respective functions based on the id ---@param id string ID of the option that was selected or deselected ---@param state boolean|any State of the option (true = enabled) function applyOptionPanelChange(id, state) + optionPanel[id] = state + -- option: Snap tags if id == "useSnapTags" then - playmatApi.setLimitSnapsByType(state, "All") - optionPanel[id] = state + playermatApi.setLimitSnapsByType(state, "All") - -- option: Draw 1 button + -- option: Draw 1 button elseif id == "showDrawButton" then - playmatApi.showDrawButton(state, "All") - optionPanel[id] = state + playermatApi.showDrawButton(state, "All") - -- option: Clickable clue counters + -- option: Use class texture + elseif id == "useClassTexture" then + playermatApi.useClassTexture(state, "All") + + -- option: Clickable clue counters elseif id == "useClueClickers" then - playmatApi.clickableClues(state, "All") - optionPanel[id] = state + playermatApi.clickableClues(state, "All") -- update master clue counter local counter = guidReferenceApi.getObjectByOwnerAndType("Mythos", "MasterClueCounter") counter.setVar("useClickableCounters", state) - -- option: Play area snap tags + -- option: Enable card helpers + elseif id == "enableCardHelpers" then + toggleCardHelpers(state) + + -- option: Play area connection drawing elseif id == "playAreaConnections" then playAreaApi.setConnectionDrawState(state) - optionPanel[id] = state - -- option: Play area connection color + -- option: Play area connection color elseif id == "playAreaConnectionColor" then playAreaApi.setConnectionColor(state) UI.setAttribute(id, "color", "#" .. Color.new(state):toHex()) - optionPanel[id] = state - -- option: Play area snap tags + -- option: Play area snap tags elseif id == "playAreaSnapTags" then playAreaApi.setLimitSnapsByType(state) - optionPanel[id] = state - -- option: Show Title on placing scenarios - elseif id == "showTitleSplash" then - optionPanel[id] = state - - -- option: Change custom playarea image on setup - elseif id == "changePlayAreaImage" then - optionPanel[id] = state - - -- option: Show clean up helper + -- option: Show clean up helper elseif id == "showCleanUpHelper" then - optionPanel[id] = spawnOrRemoveHelper(state, "Clean Up Helper", {-66, 1.6, 46}) + spawnOrRemoveHelper(state, "Clean Up Helper", { -66, 1.53, 46 }) - -- option: Show hand helper for each player + -- option: Show hand helper for each player elseif id == "showHandHelper" then spawnOrRemoveHelperForPlayermats("Hand Helper", state) - optionPanel[id] = state - -- option: Show search assistant for each player + -- option: Show search assistant for each player elseif id == "showSearchAssistant" then spawnOrRemoveHelperForPlayermats("Search Assistant", state) - optionPanel[id] = state - -- option: Show attachment helper + -- option: Show attachment helper elseif id == "showAttachmentHelper" then - optionPanel[id] = spawnOrRemoveHelper(state, "Attachment Helper", {-62, 1.4, 0}) + spawnOrRemoveHelper(state, "Attachment Helper", { -62, 1.4, 0 }) - -- option: Show CYOA campaign guides + -- option: Show CYOA campaign guides elseif id == "showCYOA" then - optionPanel[id] = spawnOrRemoveHelper(state, "CYOA Campaign Guides", { 39, 1.3, -20}) + spawnOrRemoveHelper(state, "CYOA Campaign Guides", { 39, 1.3, -20 }) - -- option: Show displacement tool + -- option: Show displacement tool elseif id == "showDisplacementTool" then - optionPanel[id] = spawnOrRemoveHelper(state, "Displacement Tool", {-57, 1.6, 46}) + spawnOrRemoveHelper(state, "Displacement Tool", { -57, 1.53, 46 }) end end @@ -1633,7 +2201,7 @@ end ---@param helperName string Name of the helper object ---@param state boolean Contains the state of the option: true = spawn it, false = remove it function spawnOrRemoveHelperForPlayermats(helperName, state) - for color, data in pairs(playmatApi.getHelperSpawnData("All", helperName)) do + for color, data in pairs(playermatApi.getHelperSpawnData("All", helperName)) do spawnOrRemoveHelper(state, helperName, data.position, data.rotation, color) end end @@ -1652,7 +2220,7 @@ function spawnOrRemoveHelper(state, name, position, rotation, owner) local cleanName = name:gsub("%s+", "") guidReferenceApi.editIndex(owner or "Mythos", cleanName, spawnedGUID) else - return removeHelperObject(name) + removeHelperObject(name) end end @@ -1676,12 +2244,10 @@ function spawnHelperObject(name, position, rotation) spawnTable.rotation = rotation end - for _, obj in ipairs(sourceBag.getData().ContainedObjects) do - if obj["Nickname"] == name then - spawnTable.data = obj - spawnTable.callback_function = function(spawnedObj) - Wait.time(function() spawnedObj.setLock(true) end, 2) - end + for _, objData in ipairs(sourceBag.getData().ContainedObjects) do + if objData["Nickname"] == name then + objData["Locked"] = true + spawnTable.data = objData return spawnObjectData(spawnTable) end end @@ -1712,19 +2278,11 @@ end -- loads the default options function onClick_defaultSettings() - for id, _ in pairs(optionPanel) do - local state = false - -- override for settings that are enabled by default - if id == "useSnapTags" or id == "showTitleSplash" then - state = true - end - applyOptionPanelChange(id, state) - end - -- clean reset of variables optionPanel = { cardLanguage = "en", changePlayAreaImage = false, + enableCardHelpers = true, playAreaConnectionColor = { a = 1, b = 0.4, g = 0.4, r = 0.4 }, playAreaConnections = true, playAreaSnapTags = true, @@ -1736,11 +2294,17 @@ function onClick_defaultSettings() showHandHelper = false, showSearchAssistant = false, showTitleSplash = true, + useClassTexture = true, useClueClickers = false, useResourceCounters = "disabled", useSnapTags = true } + -- applying changes + for id, state in pairs(optionPanel) do + applyOptionPanelChange(id, state) + end + -- update UI updateOptionPanelState() end @@ -1768,6 +2332,13 @@ function titleSplash(scenarioName) end end +-- instructs all card helpers to update their visibility +function toggleCardHelpers(state) + for _, obj in ipairs(getObjectsWithTag("CardWithHelper")) do + obj.call("setHelperState", state) + end +end + --------------------------------------------------------- -- Update notification related functionality --------------------------------------------------------- @@ -1822,8 +2393,8 @@ function updateNotificationLoading() -- update the XML UI UI.setValue("notificationHeader", "New version available: " .. modMeta["latestVersion"]) UI.setValue("releaseHighlightText", highlightText) - UI.setAttribute("highlightRow", "preferredHeight", 20*#highlights) - UI.setAttribute("updateNotification", "height", 20*#highlights + 125) + UI.setAttribute("highlightRow", "preferredHeight", 20 * #highlights) + UI.setAttribute("updateNotification", "height", 20 * #highlights + 125) end -- close / don't show again buttons on the update notification @@ -1840,6 +2411,7 @@ end -- Utility functions --------------------------------------------------------- +-- removes a value from a table function removeValueFromTable(t, val) for i, v in ipairs(t) do if v == val then @@ -1848,6 +2420,220 @@ function removeValueFromTable(t, val) end end end + +-- checks if a table is empty +function isTableEmpty(tbl) + if next(tbl) == nil then + return true + else + return false + end +end +end) +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + end + + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + end + + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") + end + + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) + end + + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) + end + + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) + end + + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) + end + + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi +end +end) +__bundle_register("chaosbag/BlessCurseManagerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local BlessCurseManagerApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getManager() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "BlessCurseManager") + end + + -- removes all taken tokens and resets the counts + BlessCurseManagerApi.removeTakenTokensAndReset = function() + local BlessCurseManager = getManager() + Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Bless") end, 0.05) + Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Curse") end, 0.10) + Wait.time(function() BlessCurseManager.call("doReset", "White") end, 0.15) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + BlessCurseManagerApi.sealedToken = function(type, guid) + getManager().call("sealedToken", { type = type, guid = guid }) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag + BlessCurseManagerApi.releasedToken = function(type, guid, fromBag) + getManager().call("releasedToken", { type = type, guid = guid, fromBag = fromBag }) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + BlessCurseManagerApi.returnedToken = function(type, guid) + getManager().call("returnedToken", { type = type, guid = guid }) + end + + -- broadcasts the current status for bless/curse tokens + ---@param playerColor string Color of the player to show the broadcast to + BlessCurseManagerApi.broadcastStatus = function(playerColor) + getManager().call("broadcastStatus", playerColor) + end + + -- removes all bless / curse tokens from the chaos bag and play + ---@param playerColor string Color of the player to show the broadcast to + BlessCurseManagerApi.removeAll = function(playerColor) + getManager().call("doRemove", playerColor) + end + + -- adds bless / curse sealing to the hovered card + ---@param playerColor string Color of the player to show the broadcast to + ---@param hoveredObject tts__Object Hovered object + BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject) + getManager().call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject }) + end + + -- adds bless / curse to the chaos bag + ---@param type string Type of chaos token ("Bless" or "Curse") + BlessCurseManagerApi.addToken = function(type) + getManager().call("addToken", type) + end + + -- removes bless / curse from the chaos bag + ---@param type string Type of chaos token ("Bless" or "Curse") + BlessCurseManagerApi.removeToken = function(type) + getManager().call("removeToken", type) + end + + BlessCurseManagerApi.getBlessCurseInBag = function() + return getManager().call("getBlessCurseInBag", {}) + end + + return BlessCurseManagerApi +end end) __bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do @@ -1862,24 +2648,24 @@ do MythosAreaApi.returnTokenData = function() return getMythosArea().call("returnTokenData") end - + ---@return any: Object reference to the encounter deck MythosAreaApi.getEncounterDeck = function() return getMythosArea().call("getEncounterDeck") end - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) end -- reshuffle the encounter deck MythosAreaApi.reshuffleEncounterDeck = function() getMythosArea().call("reshuffleEncounterDeck") end - + return MythosAreaApi end end) @@ -1939,121 +2725,77 @@ do return OptionPanelApi end end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlayAreaApi = {} + local TokenSpawnTracker = {} local guidReferenceApi = require("core/GUIDReferenceApi") - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + local function getSpawnTracker() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker") end - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + TokenSpawnTracker.hasSpawnedTokens = function(cardGuid) + return getSpawnTracker().call("hasSpawnedTokens", cardGuid) end - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") + TokenSpawnTracker.markTokensSpawned = function(cardGuid) + return getSpawnTracker().call("markTokensSpawned", cardGuid) end - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) end - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) + TokenSpawnTracker.resetAllAssetAndEvents = function() + return getSpawnTracker().call("resetAllAssetAndEvents") end - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) + TokenSpawnTracker.resetAllLocations = function() + return getSpawnTracker().call("resetAllLocations") end - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) + TokenSpawnTracker.resetAll = function() + return getSpawnTracker().call("resetAll") end - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) + return TokenSpawnTracker +end +end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/Global") +end) +__bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local TokenArrangerApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + -- local function to call the token arranger, if it is on the table + ---@param functionName string Name of the function to cal + ---@param argument? table Parameter to pass + local function callIfExistent(functionName, argument) + local tokenArranger = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenArranger") + if tokenArranger ~= nil then + tokenArranger.call(functionName, argument) + end end - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) + -- updates the token modifiers with the provided data + ---@param fullData table Contains the chaos token metadata + TokenArrangerApi.onTokenDataChanged = function(fullData) + callIfExistent("onTokenDataChanged", fullData) end - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) + -- deletes already laid out tokens + TokenArrangerApi.deleteCopiedTokens = function() + callIfExistent("deleteCopiedTokens") end - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) + -- updates the laid out tokens + TokenArrangerApi.layout = function() + Wait.time(function() callIfExistent("layout") end, 0.1) end - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) - end - - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi + return TokenArrangerApi end end) __bundle_register("core/SoundCubeApi", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -2126,6 +2868,7 @@ do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -2256,13 +2999,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -2277,11 +3020,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -2296,18 +3039,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -2319,11 +3065,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -2333,7 +3078,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -2359,16 +3108,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -2378,9 +3127,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -2416,21 +3164,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -2469,7 +3209,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -2479,11 +3219,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -2502,7 +3242,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -2519,7 +3259,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -2530,7 +3270,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -2603,21 +3343,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -2649,388 +3384,4 @@ do return TokenManager end end) -__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local TokenSpawnTracker = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getSpawnTracker() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker") - end - - TokenSpawnTracker.hasSpawnedTokens = function(cardGuid) - return getSpawnTracker().call("hasSpawnedTokens", cardGuid) - end - - TokenSpawnTracker.markTokensSpawned = function(cardGuid) - return getSpawnTracker().call("markTokensSpawned", cardGuid) - end - - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) - end - - TokenSpawnTracker.resetAllAssetAndEvents = function() - return getSpawnTracker().call("resetAllAssetAndEvents") - end - - TokenSpawnTracker.resetAllLocations = function() - return getSpawnTracker().call("resetAllLocations") - end - - TokenSpawnTracker.resetAll = function() - return getSpawnTracker().call("resetAll") - end - - return TokenSpawnTracker -end -end) -__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local PlaymatApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - local searchLib = require("util/SearchLib") - - -- Convenience function to look up a mat's object by color, or get all mats. - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@return table: Single-element if only single playmat is requested - local function getMatForColor(matColor) - if matColor == "All" then - return guidReferenceApi.getObjectsByType("Playermat") - else - return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } - end - end - - -- Returns the color of the closest playmat - ---@param startPos table Starting position to get the closest mat from - PlaymatApi.getMatColorByPosition = function(startPos) - local result, smallestDistance - for matColor, mat in pairs(getMatForColor("All")) do - local distance = Vector.between(startPos, mat.getPosition()):magnitude() - if smallestDistance == nil or distance < smallestDistance then - smallestDistance = distance - result = matColor - end - end - return result - end - - -- Returns the color of the player's hand that is seated next to the playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getPlayerColor = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("playerColor") - end - end - - -- Returns the color of the playmat that owns the playercolor's hand - ---@param handColor string Color of the playmat - PlaymatApi.getMatColor = function(handColor) - for matColor, mat in pairs(getMatForColor("All")) do - local playerColor = mat.getVar("playerColor") - if playerColor == handColor then - return matColor - end - end - end - - -- Returns if there is the card "Dream-Enhancing Serum" on the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.isDES = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("isDES") - end - end - - -- Performs a search of the deck area of the requested playmat and returns the result as table - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDeckAreaObjects = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.call("getDeckAreaObjects") - end - end - - -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.flipTopCardFromDeck = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.call("flipTopCardFromDeck") - end - end - - -- Returns the position of the discard pile of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDiscardPosition = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.call("returnGlobalDiscardPosition") - end - end - - -- Transforms a local position into a global position - ---@param localPos table Local position to be transformed - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.transformLocalPosition = function(localPos, matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.positionToWorld(localPos) - end - end - - -- Returns the rotation of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.returnRotation = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.getRotation() - end - end - - -- Returns a table with spawn data (position and rotation) for a helper object - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@param helperName string Name of the helper object - PlaymatApi.getHelperSpawnData = function(matColor, helperName) - local resultTable = {} - local localPositionTable = { - ["Hand Helper"] = {0.05, 0, -1.182}, - ["Search Assistant"] = {-0.3, 0, -1.182} - } - - for color, mat in pairs(getMatForColor(matColor)) do - resultTable[color] = { - position = mat.positionToWorld(localPositionTable[helperName]), - rotation = mat.getRotation() - } - end - return resultTable - end - - - -- Triggers the Upkeep for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@param playerColor string Color of the calling player (for messages) - PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("doUpkeepFromHotkey", playerColor) - 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) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("activeInvestigatorId") - end - end - - -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If - -- matchTypes is true, the main card slot snap points will only snap assets, while the - -- investigator area point will only snap Investigators. If matchTypes is false, snap points will - -- be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("setLimitSnapsByType", matchCardTypes) - end - end - - -- Sets the requested playmat's draw 1 button to visible - ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("showDrawButton", isDrawButtonVisible) - end - end - - -- Shows or hides the clickable clue counter for the requested playmat - ---@param showCounter boolean Whether the clickable counter should be present or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.clickableClues = function(showCounter, matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("clickableClues", showCounter) - end - end - - -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.removeClues = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("removeClues") - end - end - - -- Reports the clue count for the requested playmat - ---@param useClickableCounters boolean Controls which type of counter is getting checked - PlaymatApi.getClueCount = function(useClickableCounters, matColor) - local count = 0 - for _, mat in pairs(getMatForColor(matColor)) do - count = count + mat.call("getClueCount", useClickableCounters) - end - return count - end - - -- updates the specified owned counter - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@param type string Counter to target - ---@param newValue number Value to set the counter to - ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier - PlaymatApi.updateCounter = function(matColor, type, newValue, modifier) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) - end - end - - -- triggers the draw function for the specified playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@param number number Amount of cards to draw - PlaymatApi.drawCardsWithReshuffle = function(matColor, number) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("drawCardsWithReshuffle", number) - end - end - - -- returns the resource counter amount - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - ---@param type string Counter to target - PlaymatApi.getCounterValue = function(matColor, type) - for _, mat in pairs(getMatForColor(matColor)) do - return mat.call("getCounterValue", type) - end - end - - -- returns a list of mat colors that have an investigator placed - PlaymatApi.getUsedMatColors = function() - local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } - local usedColors = {} - - for matColor, mat in pairs(getMatForColor("All")) do - local searchPos = mat.positionToWorld(localInvestigatorPosition) - local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") - - if #searchResult > 0 then - table.insert(usedColors, matColor) - end - end - return usedColors - end - - -- resets the specified skill tracker to "1, 1, 1, 1" - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.resetSkillTracker = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("resetSkillTracker") - end - end - - -- finds all objects on the playmat and associated set aside zone and returns a table - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@param filter string Name of the filte function (see util/SearchLib) - PlaymatApi.searchAroundPlaymat = function(matColor, filter) - local objList = {} - for _, mat in pairs(getMatForColor(matColor)) do - for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do - table.insert(objList, obj) - end - end - return objList - end - - -- Discard a non-hidden card from the corresponding player's hand - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.doDiscardOne = function(matColor) - for _, mat in pairs(getMatForColor(matColor)) do - mat.call("doDiscardOne") - end - end - - -- Triggers the metadata sync for all playmats - PlaymatApi.syncAllCustomizableCards = function() - for _, mat in pairs(getMatForColor("All")) do - mat.call("syncAllCustomizableCards") - end - end - - return PlaymatApi -end -end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } - - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] - end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) - - -- filtering the result - local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) - end - end - return objList - end - - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) - end - - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) - end - - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib -end -end) return __bundle_require("__root") diff --git a/unpacked.xml b/unpacked.xml index 09a97a89e..a43ce0bdc 100644 --- a/unpacked.xml +++ b/unpacked.xml @@ -567,16 +567,13 @@ + alignment="MiddleLeft"/> - - - + + outline="grey"/> + animationDuration="0.2"/> + preferredHeight="44"/> + color="#222222"/> + padding="5 0 0 0"/> + font="font_teutonic-arkham"/> + + + + + + + Document settings: + Height + ]] .. + height .. [[ + Scale + ]] .. + scale.x .. [[ + ]] .. + scale.y .. [[ + + + Lock everything + Flip + + + + + + + + + + + + + ]] .. selectedId .. + " / " .. getPageCount() .. [[ + + + + + Add a new element using the buttons above. You can then select elements by clicking on them on the sheet, or with the navigation buttons above. + + + ]] .. getFieldPanel() .. [[ + ]] .. getCheckPanel() .. [[ + ]] .. getDecalPanel() .. [[ + + + + + Loading... + + + + ]] + UI.setXml(UI.getXml() .. xml) +end + +function onScaleReset() + scale = { x = math.floor(100 / self.getScale().x) / 100, y = math.floor(100 / self.getScale().z) / 100 } + UI.setAttribute(attrId("ScaleX"), "text", scale.x) + UI.setAttribute(attrId("ScaleY"), "text", scale.y) + refreshAllPositionsAndSize() + refreshEditPanel() +end + +function firstPage() + if (#fields > 0) then + selectField({ id = 1, arrayId = 1 }) + elseif (#checks > 0) then + selectCheck({ id = 1, arrayId = 1 }) + elseif (#decals > 0) then + selectDecal(1) + end +end + +function previousPage() + if (selectedType == "decal") then + if (selectedId > 1) then + selectedId = selectedId - 1 + selectDecal(selectedId) + else + if (#checks > 0) then + selectedType = "check" + selectedId = #checks + selectCheck({ id = selectedId, arrayId = checks[selectedId].array.x * checks[selectedId].array.y }) + elseif (#fields > 0) then + selectedType = "field" + selectedId = #fields + selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y }) + end + end + elseif (selectedType == "check") then + if (selectedArrayId > 1) then + selectedArrayId = selectedArrayId - 1 + selectCheck({ id = selectedId, arrayId = selectedArrayId }) + elseif (selectedId > 1) then + selectedId = selectedId - 1 + selectCheck({ id = selectedId, arrayId = checks[selectedId].array.x * checks[selectedId].array.y }) + else + if (#fields > 0) then + selectedType = "field" + selectedId = #fields + selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y }) + end + end + elseif (selectedType == "field") then + if (selectedArrayId > 1) then + selectedArrayId = selectedArrayId - 1 + selectField({ id = selectedId, arrayId = selectedArrayId }) + elseif (selectedId > 1) then + selectedId = selectedId - 1 + selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y }) + end + end +end + +function nextPage() + if (selectedType == "field") then + if (fields[selectedId].array.x * fields[selectedId].array.y > selectedArrayId) then + selectedArrayId = selectedArrayId + 1 + selectField({ id = selectedId, arrayId = selectedArrayId }) + elseif (#fields > selectedId) then + selectedId = selectedId + 1 + selectField({ id = selectedId, arrayId = 1 }) + else + if (#checks > 0) then + selectedType = "check" + selectedId = 1 + selectCheck({ id = selectedId, arrayId = 1 }) + elseif (#decals > 0) then + selectedType = "decal" + selectedId = 1 + selectDecal(1) + end + end + elseif (selectedType == "check") then + if (checks[selectedId].array.x * checks[selectedId].array.y > selectedArrayId) then + selectedArrayId = selectedArrayId + 1 + selectCheck({ id = selectedId, arrayId = selectedArrayId }) + elseif (#checks > selectedId) then + selectedId = selectedId + 1 + selectCheck({ id = selectedId, arrayId = 1 }) + else + if (#decals > 0) then + selectedType = "decal" + selectedId = 1 + selectDecal(1) + end + end + elseif (selectedType == "decal") then + if (#decals > selectedId) then + selectedId = selectedId + 1 + selectDecal(selectedId) + end + end +end + +function lastPage() + if (#decals > 0) then + selectDecal(#decals) + elseif (#checks > 0) then + local last = checks[#checks] + selectCheck({ id = #checks, arrayId = last.array.x * last.array.y }) + elseif (#fields > 0) then + local last = fields[#fields] + selectField({ id = #fields, arrayId = last.array.x * last.array.y }) + end +end + +function onNudgeChanged(ply, value, id) + nudgeDistance = value + UI.setAttribute(attrId("Field_nudge"), "text", nudgeDistance) + UI.setAttribute(attrId("Check_nudge"), "text", nudgeDistance) + UI.setAttribute(attrId("Decal_nudge"), "text", nudgeDistance) + updateSave() +end + +function compareString(str, list) + for k, v in pairs(list) do + if (str == v) then return true end + end +end + +function onValueEdited(ply, value, id) + local spl = split(id, "/") + local parameter = spl[2] + local tbl = nil + if (selectedType == "field") then tbl = fields end + if (selectedType == "check") then tbl = checks end + if (selectedType == "decal") then tbl = decals end + if (parameter == "content") then + tbl[selectedId].value[selectedArrayId] = value + else + if (#spl > 2) then + tbl[selectedId][parameter][spl[3]] = value + else + tbl[selectedId][parameter] = value + end + end + updateSave() + + if (selectedType == "field") then + if (compareString(parameter, { "pos", "distance", "size" })) then + updateFieldPositionAndSize(selectedId) + elseif (compareString(parameter, { "name", "content", "tooltip" })) then + updateFieldNameContentAndTooltip(selectedId) + elseif (parameter == "font") then + updateFieldFontAndColor(selectedId) + else + refresh() + end + elseif (selectedType == "check") then + if (compareString(parameter, { "pos", "distance", "size" })) then + updateCheckPositionAndSize(selectedId) + elseif (parameter == "name") then + updateCheckNameContentAndTooltip(selectedId) + elseif (parameter == "font") then + updateCheckFontAndColor(selectedId) + else + refresh() + end + elseif (selectedType == "decal") then + if (compareString(parameter, { "pos", "scale", "rotation" })) then + updateDecalPositionAndSize(selectedId) + elseif (parameter == "url") then + createDecals() + else + refresh() + end + end +end + +function updateFieldPositionAndSize(fieldID) + local field = fields[fieldID] + local lookup = lookupFieldIndices[fieldID] + local fieldScale = { x = -scale.x, y = 1, z = scale.y } + local flipped = 1 + local rotation = { x = 0, y = 0, z = 180 } + if (flip == "True") then + rotation.y = 180 + flipped = -1 + end + local fontSize = math.min(field.size.y - 24, field.font) + for k, v in pairs(lookup.inputs) do + local pos = getFieldPosition(fieldID, v.x, v.y) + self.editInput({ + index = v.index - 1, + position = pos, + scale = fieldScale, + width = field.size.x, + height = field.size.y, + font_size = fontSize, + rotation = rotation + }) + end + for k, v in pairs(lookup.totals) do + local pos = getFieldPosition(fieldID, v.x, field.array.y + 1) + self.editInput({ + index = v.index - 1, + position = pos, + scale = fieldScale, + width = field.size.x, + height = field.size.y, + rotation = rotation, + font_size = fontSize + }) + end + for k, v in pairs(lookup.counterButtons) do + local pos = getFieldPosition(fieldID, v.x, v.y) + local offset = (field.size.x + fontSize * 0.75) / 1000 * scale.x + self.editButton({ + index = v.index - 1, + position = { x = pos.x + offset * v.side * flipped, y = pos.y, z = pos.z }, + scale = fieldScale, + rotation = rotation + }) + end + for k, v in pairs(lookup.selectionButtons) do + local pos = getFieldPosition(fieldID, v.x, v.y) + self.editButton({ + index = v.index - 1, + position = pos, + scale = { x = scale.x * 0.25, y = 1, z = scale.y * 0.25 }, + width = field.size.x * 4 + 200, + height = field.size.y * 4 + 200 + }) + end + createSelectionHighlight(true) +end + +function updateFieldFontAndColor(fieldID) + local field = fields[fieldID] + local fontColor = getFieldTextColor(fieldID) + local fontSize = math.min(field.size.y - 24, field.font) + local lookup = lookupFieldIndices[fieldID] + for k, v in pairs(lookup.inputs) do + -- Do not question the ways of the tabletop, as if it requests for font_color to be assigned twice, then it shall be so + self.editInput({ + index = v.index - 1, + font_color = fontColor + }) + self.editInput({ + index = v.index - 1, + font_size = fontSize, + color = field.fieldColor, + font_color = fontColor + }) + self.editInput({ + index = v.index - 1 + }) + end + for k, v in pairs(lookup.counterButtons) do + self.editButton({ + index = v.index - 1, + color = field.fieldColor, + font_color = fontColor, + font_size = fontSize / 2 + }) + end + for k, v in pairs(lookup.totals) do + self.editInput({ + index = v.index - 1, + font_size = fontSize, + color = field.fieldColor, + font_color = fontColor + }) + end +end + +function updateCheckNameContentAndTooltip(checkID) + local check = checks[checkID] + for k, v in pairs(lookupCheckIndices[checkID].buttons) do + local label = getCheckLabelAndColor(checkID, v.arrayID) + self.editButton({ index = v.index - 1, label = label, tooltip = getCheckTooltip(checkID) }) + end + local name = "C" .. checkID + local tooltip = "Select " .. (check.name or name) + for k, v in pairs(lookupCheckIndices[checkID].selectionButtons) do + self.editButton({ index = v.index - 1, tooltip = tooltip }) + end +end + +function updateCheckPositionAndSize(checkID) + local check = checks[checkID] + local lookup = lookupCheckIndices[checkID] + local rotation = { x = 0, y = 0, z = 0 } + if (flip == "True") then + rotation.y = 180 + end + for k, v in pairs(lookup.buttons) do + local pos = getCheckPosition(checkID, v.x, v.y) + local checkScale = { x = scale.x * check.size.x, y = 1, z = scale.y * check.size.y } + self.editButton({ index = v.index - 1, position = pos, scale = checkScale, rotation = rotation }) + end + for k, v in pairs(lookup.selectionButtons) do + local pos = getCheckPosition(checkID, v.x, v.y) + local checkScale = { x = scale.x * check.size.x * 0.25, y = 1, z = scale.y * check.size.y * 0.25 } + self.editButton({ index = v.index - 1, position = pos, scale = checkScale }) + end + createSelectionHighlight(true) +end + +function updateCheckFontAndColor(checkID) + local check = checks[checkID] + for k, v in pairs(lookupCheckIndices[checkID].buttons) do + local label, color, alphaCorrectedColor = getCheckLabelAndColor(checkID, v.arrayID) + -- Do not question the ways of the tabletop, as if it requests for font_color to be assigned twice, then it shall be so + self.editButton({ + index = v.index - 1, + font_size = check.font, + color = check.checkColor, + font_color = alphaCorrectedColor + }) + end +end + +function updateDecalPositionAndSize(decalID) + local decal = decals[decalID] + local lookup = lookupDecalIndices[decalID] + local decalScale = { + x = decal.scale.x * scale.x * 0.25, + y = decal.scale.y * scale.y * 0.25, + z = decal.scale.y * + scale.y * 0.25 + } + for k, v in pairs(lookup.inputs) do + local pos = getDecalPosition(decalID, v.x, v.y) + self.editInput({ index = v.index - 1, position = pos, scale = decalScale, rotation = decal.rotation }) + end + for k, v in pairs(lookup.selectionButtons) do + local pos = getDecalPosition(decalID, v.x, v.y) + self.editButton({ index = v.index - 1, position = pos, scale = decalScale }) + end + createSelectionHighlight(true) + createDecals() +end + +function onCheckValueEdited(ply, value, id) + local spl = split(id, "/") + local checkParameter = spl[2] + if (#spl > 2) then + checks[selectedId][checkParameter][spl[3]] = value + else + checks[selectedId][checkParameter] = value + end + + if (compareString(checkParameter, { "pos", "size" })) then + updateCheckPositionAndSize(selectedId) + elseif (checkParameter == "characters") then + updateCheckNameContentAndTooltip(selectedId) + else + refresh() + end + + updateSave() +end + +function onDecalValueEdited(ply, value, id) + local spl = split(id, "/") + local decalID = tonumber(spl[1]) + local decalParameter = spl[2] + if (#spl > 2) then + decals[decalID][decalParameter][spl[3]] = value + else + decals[decalID][decalParameter] = value + end + + updateSave() + refresh() +end + +function onToggleChanged(ply, value, id) + local spl = split(id, "/") + local tbl = nil + if (selectedType == "field") then tbl = fields end + if (selectedType == "check") then tbl = checks end + if (selectedType == "decal") then tbl = decals end + tbl[selectedId][spl[2]] = value + if (spl[2] == "separateColors") then + updateCheckFontAndColor(selectedId) + refreshEditPanel() + else + if (not compareString(spl[2], { "locked", "fillFromDisabled" })) then + refresh() + end + end + updateSave() +end + +function onFieldDropdownSelected(ply, option, id) + local spl = split(id, "/") + local parameterID = spl[2] + local shouldRefresh = false + if (parameterID == "align") then + shouldRefresh = true + if (option == "Auto") then + option = 1 + elseif (option == "Left") then + option = 2 + elseif (option == "Center") then + option = 3 + elseif (option == "Right") then + option = 4 + elseif (option == "Justified") then + option = 5 + end + end + fields[selectedId][parameterID] = option + updateFieldFontAndColor(selectedId) + updateSave() + if (shouldRefresh) then + -- I did my best to not refresh here, but it seems like editInput doesn't care about alignment + refresh() + end +end + +function onCheckDropdownSelected(ply, option, id) + local spl = split(id, "/") + local parameterID = spl[2] + if (parameterID == "value") then + local value = 0 + if (option == "Off") then value = 1 elseif (option == "On") then value = 2 end + local check = checks[selectedId] + check.value[selectedArrayId] = value + updateCheckNameContentAndTooltip(selectedId) + updateCheckFontAndColor(selectedId) + updateSave() + elseif (parameterID == "tooltip") then + checks[selectedId].tooltip = option + updateSave() + else + checks[selectedId][parameterID] = option + updateSave() + refresh() + end +end + +function onDecalDropdownSelected(ply, option, id) + local spl = split(id, "/") + local parameterID = spl[2] + if (parameterID == "tooltip") then + decals[selectedId].tooltip = option + updateSave() + else + decals[selectedId][parameterID] = option + updateSave() + refresh() + end +end + +function onColorButtonPressed(ply, value, id) + local spl = split(id, "/") + local parameterID = spl[2] + local tbl = nil + if (selectedType == "field") then tbl = fields end + if (selectedType == "check") then tbl = checks end + local startingColor = tbl[selectedId][parameterID] + if (startingColor == nil) then + if (parameterID:find("textColor")) then + startingColor = tbl[selectedId].textColor + end + end + + ply.showColorDialog(startingColor, + function(color, player_color) + tbl[selectedId][parameterID] = color + if (selectedType == "field") then + updateFieldFontAndColor(selectedId) + elseif (selectedType == "check") then + updateCheckFontAndColor(selectedId) + end + local c = "rgba(" .. color.r .. "," .. color.g .. "," .. color.b .. ",1)" + local c2 = "rgba(" .. (color.r * 0.5 + 0.2) .. "," .. (color.g * 0.5 + 0.2) .. "," .. + (color.b * 0.5 + 0.2) .. ",1)" + UI.setAttribute(id, "colors", c .. "|" .. c2 .. "|" .. c2 .. "|" .. c) + UI.setAttribute(id .. "/a", "percentage", color.a * 100) + updateSave() + end + ) +end + +function refreshAllPositionsAndSize() + for k, v in pairs(fields) do + updateFieldPositionAndSize(k) + end + for k, v in pairs(checks) do + updateCheckPositionAndSize(k) + end + for k, v in pairs(decals) do + updateDecalPositionAndSize(k) + end +end + +function onScaleChangedX(ply, value) + scale.x = value + updateSave() + refreshAllPositionsAndSize() +end + +function onScaleChangedY(ply, value) + scale.y = value + updateSave() + refreshAllPositionsAndSize() +end + +function onHeightChanged(ply, value) + height = value + updateSave() + refreshAllPositionsAndSize() +end + +function onFlip(ply, value) + flip = value + updateSave() + refreshAllPositionsAndSize() + local label = "┗" + if (flip == "True") then label = "┓" end + self.editButton({ index = 0, label = label }) + label = "┏" + if (flip == "True") then label = "┛" end + self.editButton({ index = 1, label = label }) + label = "┛" + if (flip == "True") then label = "┏" end + self.editButton({ index = 2, label = label }) + label = "┓" + if (flip == "True") then label = "┗" end + self.editButton({ index = 3, label = label }) +end + +function onCheckToggleChanged(ply, value, id) + local spl = split(id, "/") + local checkID = tonumber(spl[1]) + checks[checkID][spl[2]] = value + + updateSave() + refresh() +end + +function onDecalToggleChanged(ply, value, id) + local spl = split(id, "/") + local decalID = tonumber(spl[1]) + decals[decalID][spl[2]] = value + + updateSave() + refresh() +end + +function getDeselectButton(panelID) + if (selectedId > 0) then + return [[]] + end + return "" +end + +function deselect() + selectedId = 0 + selectedType = "" + refresh() +end + +function correctCheckboxesAndDecals() + for decalID, decal in pairs(decals) do + decal.pos.x = decal.pos.x / scale.x + decal.pos.y = decal.pos.y / scale.y + end + for checkID, check in pairs(checks) do + check.pos.x = check.pos.x / scale.x + check.pos.y = check.pos.y / scale.y + check.distance.x = check.distance.x / scale.x + check.distance.y = check.distance.y / scale.y + end + refresh() +end + +function attrId(str) + return "MarumEditorSheetAttribute_" .. str +end + +function getFieldPanel() + local guid = self.getGUID() + return [[ + + + + + + Text ? + + + + + + + + + + + + + + + Text + + + + + Name + + + + + + + + + Content + + + + + + + + + Tooltip + + + + + + + + + + + + + On edit + + + + + + + + + + + + + + Font and color + + + + + Font size + + + + + + + + + Align + + + + + + + + + + + + + Text color + + + + + + + + + + + + Background Color + + + + + + + + + + + + + + Position and Size + + + + + + + + + Nudge + + + + + ]] .. + nudgeDistance .. [[ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pos + + + + + + + + + + + Size + + + + + + + + + + + + + Array + + + + + Columns/Rows + + + + + + + + + + + Spacing + + + + + + + + + + + + + Toggles + + + + + Counter + + + Total sum + + + Lock + + + + + + + ]] +end + +function getCheckPanel() + local guid = self.getGUID() + return [[ + + + + + + Checkbox ? + + + + + + + + + + + + + + + Checkbox + + + + + Name + + + + + + + + + State + + + + + + + + + + + + + Tooltip + + + + + + + + + + + + + + + + Off + + + + + + On + + + + + + Disabled + + + + + + + + + + + + + + Font size + + + + + + + + Separate colors + + + + + On color + + + + + + + + + + + Off color + + + + + + + + + + + Disabled color + + + + + + + + + + + Background Color + + + + + + + + + + + + + + + + Position and Scale + + + + + + + + + Nudge + + + + + ]] .. + nudgeDistance .. [[ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pos + + + + + + + + + + + Scale + + + + + + + + + + + + + Array + + + + + Columns/Rows + + + + + + + + + + + Spacing + + + + + + + + + + + + + Toggles + + + + + Can fill even if disabled + + + Lock + + + + + + + ]] +end + +function getDecalPanel() + local guid = self.getGUID() + return [[ + + + + + + Image ? + + + + + + + + + + + + + + + Image + + + + + + Name + + + + + + + + + URL + + + + + + + + + Tooltip + + + + + + + + + + + + + + + + + Position and Scale + + + + + + + + + Nudge + + + + + ]] .. + nudgeDistance .. [[ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pos + + + + + + + + + + + Scale + + + + + + + + + + + Rotation + + + + + + + + + + Toggles + + + + + Lock + + + + + + + + ]] +end + +function onCheckPresetButton(ply, value, id) + local pres = { "◌ ○ ●", " ◇ ◆", " □ ■", "/ △ ▴", " ◎ ◉", "- x", " ◾ ✦", " x ♥" } + ply.showOptionsDialog("Select checkbox preset", pres, 1, + function(text, index, player_color) + local disabled = pres[index]:sub(1, 1) + local empty = pres[index]:sub(3, 3) + local filled = pres[index]:sub(5, 5) + checks[selectedId].characters.disabled = disabled + checks[selectedId].characters.empty = empty + checks[selectedId].characters.filled = filled + updateCheckNameContentAndTooltip(selectedId) + refreshEditPanel() + updateSave() + end) +end + +function nudgeSet1() updateNudgeDistance(1) end + +function nudgeSet01() updateNudgeDistance(0.1) end + +function nudgeSet001() updateNudgeDistance(0.01) end + +function nudgeSet0001() updateNudgeDistance(0.001) end + +function updateNudgeDistance(value) + nudgeDistance = value + UI.setAttribute(attrId("Field_nudge"), "text", nudgeDistance) + UI.setAttribute(attrId("Check_nudge"), "text", nudgeDistance) + UI.setAttribute(attrId("Decal_nudge"), "text", nudgeDistance) + updateSave() +end + +function nudgeLeft(obj, value, id) + local newX = fields[selectedId].pos.x - tonumber(nudgeDistance) + UI.setAttribute(attrId("Field/pos/x"), "text", newX) + fields[selectedId].pos.x = newX + updateFieldPositionAndSize(selectedId) + updateSave() +end + +function nudgeUp(obj, value, id) + local newY = fields[selectedId].pos.y - tonumber(nudgeDistance) + UI.setAttribute(attrId("Field/pos/y"), "text", newY) + fields[selectedId].pos.y = newY + updateFieldPositionAndSize(selectedId) + updateSave() +end + +function nudgeRight(obj, value, id) + local newX = fields[selectedId].pos.x + tonumber(nudgeDistance) + UI.setAttribute(attrId("Field/pos/x"), "text", newX) + fields[selectedId].pos.x = newX + updateFieldPositionAndSize(selectedId) + updateSave() +end + +function nudgeDown(obj, value, id) + local newY = fields[selectedId].pos.y + tonumber(nudgeDistance) + UI.setAttribute(attrId("Field/pos/y"), "text", newY) + fields[selectedId].pos.y = newY + updateFieldPositionAndSize(selectedId) + updateSave() +end + +function nudgeDecalLeft(obj, value, id) + local newX = decals[selectedId].pos.x - tonumber(nudgeDistance) + UI.setAttribute(attrId("Decal/pos/x"), "text", newX) + decals[selectedId].pos.x = newX + updateDecalPositionAndSize(selectedId) + updateSave() +end + +function nudgeDecalUp(obj, value, id) + local newY = decals[tonumber(selectedId)].pos.y - tonumber(nudgeDistance) + UI.setAttribute(attrId("Decal/pos/y"), "text", newY) + decals[tonumber(selectedId)].pos.y = newY + updateDecalPositionAndSize(selectedId) + updateSave() +end + +function nudgeDecalRight(obj, value, id) + local newX = decals[tonumber(selectedId)].pos.x + tonumber(nudgeDistance) + UI.setAttribute(attrId("Decal/pos/x"), "text", newX) + decals[tonumber(selectedId)].pos.x = newX + updateDecalPositionAndSize(selectedId) + updateSave() +end + +function nudgeDecalDown(obj, value, id) + local newY = decals[tonumber(selectedId)].pos.y + tonumber(nudgeDistance) + UI.setAttribute(attrId("Decal/pos/y"), "text", newY) + decals[tonumber(selectedId)].pos.y = newY + updateDecalPositionAndSize(selectedId) + updateSave() +end + +function nudgeCheckLeft(obj, value, id) + local newX = checks[tonumber(selectedId)].pos.x - tonumber(nudgeDistance) + UI.setAttribute(attrId("Check/pos/x"), "text", newX) + checks[tonumber(selectedId)].pos.x = newX + updateCheckPositionAndSize(selectedId) + updateSave() +end + +function nudgeCheckUp(obj, value, id) + local newY = checks[tonumber(selectedId)].pos.y - tonumber(nudgeDistance) + UI.setAttribute(attrId("Check/pos/y"), "text", newY) + checks[tonumber(selectedId)].pos.y = newY + updateCheckPositionAndSize(selectedId) + updateSave() +end + +function nudgeCheckRight(obj, value, id) + local newX = checks[tonumber(selectedId)].pos.x + tonumber(nudgeDistance) + UI.setAttribute(attrId("Check/pos/x"), "text", newX) + checks[tonumber(selectedId)].pos.x = newX + updateCheckPositionAndSize(selectedId) + updateSave() +end + +function nudgeCheckDown(obj, value, id) + local newY = checks[tonumber(selectedId)].pos.y + tonumber(nudgeDistance) + UI.setAttribute(attrId("Check/pos/y"), "text", newY) + checks[tonumber(selectedId)].pos.y = newY + updateCheckPositionAndSize(selectedId) + updateSave() +end + +function showEmptyPrompt() + selectedId = 0 + selectedType = "" + refreshEditPanel() +end + +function duplicateField(ply, value, id) + broadcastToColor("Text duplicated", ply.color) + local newField = JSON.decode(JSON.encode(fields[tonumber(selectedId)])) + table.insert(fields, newField) + updateSave() + selectField({ id = #fields, arrayId = 1 }) + refresh() +end + +function deleteField(ply, value, id) + broadcastToColor("Text deleted", ply.color) + table.remove(fields, tonumber(selectedId)) + updateSave() + if (#fields > 0) then + selectField({ id = math.max(selectedId - 1, 1), arrayId = 1 }) + else + if (#checks > 0) then + selectCheck({ id = 1, arrayId = 1 }) + elseif (#decals > 0) then + selectDecal(1) + else + showEmptyPrompt() + end + end + refresh() +end + +function duplicateCheck(ply, value, id) + broadcastToColor("Check duplicated", ply.color) + local newCheck = JSON.decode(JSON.encode(checks[tonumber(selectedId)])) + table.insert(checks, newCheck) + updateSave() + selectCheck({ id = #checks, arrayId = 1 }) + refresh() +end + +function deleteCheck(ply, value, id) + broadcastToColor("Check deleted", ply.color) + table.remove(checks, tonumber(selectedId)) + updateSave() + if (#checks > 0) then + selectCheck({ id = math.max(selectedId - 1, 1), arrayId = 1 }) + else + if (#fields > 0) then + selectField({ id = 1, arrayId = 1 }) + elseif (#decals > 0) then + selectDecal(1) + else + showEmptyPrompt() + end + end + refresh() +end + +function duplicateDecal(ply, value, id) + broadcastToColor("Image duplicated", ply.color) + local newDecal = JSON.decode(JSON.encode(decals[tonumber(selectedId)])) + table.insert(decals, newDecal) + updateSave() + selectDecal(#decals) + refresh() +end + +function deleteDecal(ply, value, id) + broadcastToColor("Image deleted", ply.color) + table.remove(decals, tonumber(selectedId)) + updateSave() + if (#decals > 0) then + selectDecal(math.max(selectedId - 1, 1)) + else + if (#fields > 0) then + selectField({ id = 1, arrayId = 1 }) + elseif (#checks > 0) then + selectCheck({ id = 1, arrayId = 1 }) + else + showEmptyPrompt() + end + end + refresh() +end + +function addField(obj, player_clicker_color, alt_click) + local newField = { + value = { "?" }, + name = "", + tooltip = "name", + role = "Normal Field", + textColor = { r = 0, g = 0, b = 0, a = 1 }, + fieldColor = { r = 1, g = 1, b = 1, a = 1 }, + font = 150, + align = 3, + pos = { x = 0, y = 0 }, + size = { x = 250, y = 250 }, + array = { x = 1, y = 1 }, + distance = { x = 1, y = 1 }, + locked = false + } + table.insert(fields, newField) + selectField({ id = #fields, arrayId = 1 }) + updateSave() + refresh() +end + +function addCheck(obj, player_clicker_color, alt_click) + local newCheck = { + name = "", + tooltip = "hint", + value = { 1 }, + characters = { disabled = "◌", empty = "○", filled = "●" }, + textColor = { r = 0, g = 0, b = 0, a = 1 }, + textColorOff = { r = 0, g = 0, b = 0, a = 1 }, + textColorDisabled = { r = 0.5, g = 0.5, b = 0.5, a = 1 }, + separateColors = false, + fillFromDisabled = false, + checkColor = { r = 1, g = 1, b = 1, a = 1 }, + pos = { x = 0, y = 0 }, + size = { x = 1, y = 1 }, + array = { x = 1, y = 1 }, + distance = { x = 1, y = 1 }, + font = 500, + locked = false + } + table.insert(checks, newCheck) + selectCheck({ id = #checks, arrayId = 1 }) + updateSave() + refresh() +end + +function addDecal(obj, player_clicker_color, alt_click) + local newDecal = { + name = "", + url = "https://api.tabletopsimulator.com/img/TSIcon.png", + tooltip = "name", + pos = { x = 0, y = 0 }, + rotation = 0, + scale = { x = 1, y = 1 }, + locked = false + } + table.insert(decals, newDecal) + selectDecal(#decals) + updateSave() + refresh() +end + +function onDestroy() + closePanel() +end + +function cutAtWord(inputStr, delimiter) + local t = {} + local pattern = "(.-)" .. delimiter + for str in inputStr:gmatch(pattern) do + table.insert(t, str) + end + if (#t > 0) then + return t[1] + else + return false + end +end + +function waitForUiLoaded(callback) + if UI.loading == false then + callback() + return nil + end + + return Wait.condition( + function() + callback() + end, + function() + return UI.loading == false + end + ) +end \ No newline at end of file diff --git a/unpacked/Custom_Tile Patch Notes f47225.yaml b/unpacked/Custom_Tile Patch Notes f47225.yaml new file mode 100644 index 000000000..cc4354ab6 --- /dev/null +++ b/unpacked/Custom_Tile Patch Notes f47225.yaml @@ -0,0 +1,116 @@ +AltLookAngle: + x: 0 + y: 0 + z: 0 +AttachedDecals: +- CustomDecal: + ImageURL: http://cloud-3.steamusercontent.com/ugc/2501268517218943111/803E57A7B3E9765DF342050EE6C71D69473A7388/ + Name: 'Image #1' + Size: 1 + Transform: + posX: -0.93 + posY: 0.105 + posZ: 0.66 + rotX: 90 + rotY: 180 + rotZ: 0 + scaleX: 0.6 + scaleY: 0.6 + scaleZ: 1 +- CustomDecal: + ImageURL: http://cloud-3.steamusercontent.com/ugc/2037357792052848566/5DA900C430E97D3DFF2C9B8A3DB1CB2271791FC7/ + Name: 'Image #2' + Size: 1 + Transform: + posX: -1.05 + posY: 0.105 + posZ: -0.567 + rotX: 90 + rotY: 205 + rotZ: 0 + scaleX: 0.3 + scaleY: 0.3 + scaleZ: 1 +- CustomDecal: + ImageURL: http://cloud-3.steamusercontent.com/ugc/2501268517219098388/0936FEE03B410319658B5E05DB5D486CEDDE98F5/ + Name: 'Image #3' + Size: 1 + Transform: + posX: 0 + posY: 0.105 + posZ: -0.81 + rotX: 90 + rotY: 180 + rotZ: 0 + scaleX: 2.4 + scaleY: 0.009 + scaleZ: 1 +Autoraise: true +ColorDiffuse: + b: 1 + g: 1 + r: 1 +CustomImage: + CustomTile: + Stackable: false + Stretch: true + Thickness: 0.1 + Type: 0 + ImageScalar: 1 + ImageSecondaryURL: http://sfwallpaper.com/images/parchment-paper-wallpaper-10.jpg + ImageURL: http://sfwallpaper.com/images/parchment-paper-wallpaper-10.jpg + WidthScale: 0 +Description: '' +DragSelectable: true +GMNotes: '' +GUID: f47225 +Grid: true +GridProjection: false +Hands: false +HideWhenFaceDown: false +IgnoreFoW: false +LayoutGroupSortIndex: 0 +Locked: false +LuaScript: !include 'Custom_Tile Patch Notes f47225.ttslua' +LuaScriptState: '{"checks":[],"decals":[{"locked":false,"name":"Arkham SCE logo","pos":{"x":3.1,"y":2.2},"rotation":0,"scale":{"x":"2","y":"2"},"tooltip":"None","url":"http://cloud-3.steamusercontent.com/ugc/2501268517218943111/803E57A7B3E9765DF342050EE6C71D69473A7388/"},{"locked":false,"name":"Bootlegger + Finn","pos":{"x":3.5,"y":-1.89},"rotation":"25","scale":{"x":"1","y":"1"},"tooltip":"None","url":"http://cloud-3.steamusercontent.com/ugc/2037357792052848566/5DA900C430E97D3DFF2C9B8A3DB1CB2271791FC7/"},{"locked":false,"name":"black + bar","pos":{"x":0,"y":-2.7},"rotation":0,"scale":{"x":"8","y":"0.03"},"tooltip":"None","url":"http://cloud-3.steamusercontent.com/ugc/2501268517219098388/0936FEE03B410319658B5E05DB5D486CEDDE98F5/"}],"fields":[{"align":3,"array":{"x":"1","y":"1"},"counter":"False","distance":{"x":"1","y":"1"},"fieldColor":{"a":0,"b":1,"g":1,"r":1},"font":"200","locked":false,"name":"Patch + Notes","pos":{"x":"0","y":-2.9},"role":"Normal Field","size":{"x":"3750","y":"250"},"textColor":{"a":1,"b":0,"g":0,"r":0},"tooltip":"None","value":["Arkham + Horror LCG SCE 3.9.0 - 07/08/2024"]},{"align":2,"array":{"x":"1","y":1},"distance":{"x":"1","y":"1"},"fieldColor":{"a":0,"b":1,"g":1,"r":1},"font":"89","locked":false,"name":"Details","pos":{"x":"0","y":0.4},"role":"Nothing","size":{"x":"3750","y":"2750"},"textColor":{"a":1,"b":0,"g":0,"r":0},"tooltip":"None","value":["Thanks + for downloading! We''re happy to present you a rather big update this time :-)\n\nNew + things\n- updated note card for patch notes (bless Marum for his awesome tool!)\n- + automated discarding for Patrice\n- confirmation dialog for discard hotkey (e.g. + for locations)\n- helpers for cards that redraw tokens and Kohaku\n- displaying + of token count for cards that seal tokens\r\n- new action / ability tokens (replacing + the old ones)\r\n- option to enable all card helpers (e.g. Heavy Furs)\r\n- option + to load class-colored playermat backgrounds\n- coloring for player names in broadcasts\n- + right-click option for RBW button on Player Card Panel to specify trait(s)\n\nUpdates\r\n- + performed a small clean up of the bottom corners of the table\n- \"Numpad 9\" to + rearranges present tokens (on top of adding a resource)\n- Scroll of Secrets context + menu helper now displays player names instead of colors\r\n- Player Card Panel can + display fan-made cards with a new \"custom\" cycle button)\n- updated Family Inheritance + helper to a proper UI\n- \"Discard object\" gamekey works for selected objects\r\n- + updated a bunch of tools like Clean Up Helper, Drawing Tool,\nHand Helper, Token + Arranger and Search Assistant\n\nFixes\r\r\n- Bugfix for attempting to draw an encounter + card while there is no deck\r\n- Bugfix for Navigation Overlay: now checks if playmat + is occupied\r\n- Bugfix for Phase Tracker broadcasting\r\n- Performance and file + size improvements (e.g. by adding download\nfunctions for CYOA campaign guides and + Arkham Fantasy standees)"]}],"flip":"False","height":"0.1","locks":{"checks":false,"decals":false,"fields":false},"nudgeDistance":0.1,"scale":{"x":"0.3","y":"0.3"},"sheetLocked":true}' +MeasureMovement: false +Name: Custom_Tile +Nickname: Patch Notes +Snap: true +Sticky: true +Tooltip: true +Transform: + posX: -27 + posY: 1.48 + posZ: -56.16 + rotX: 0 + rotY: 270 + rotZ: 0 + scaleX: 7.5 + scaleY: 1 + scaleZ: 7.5 +Value: 0 +XmlUI: '' diff --git a/unpacked/Custom_Tile Phase Tracker d0c8fa.ttslua b/unpacked/Custom_Tile Phase Tracker d0c8fa.ttslua index e12f8d317..8ebcb5c09 100644 --- a/unpacked/Custom_Tile Phase Tracker d0c8fa.ttslua +++ b/unpacked/Custom_Tile Phase Tracker d0c8fa.ttslua @@ -65,11 +65,15 @@ function onSave() }) end +function loadFromSaveTable(savedData) + for var, val in pairs(JSON.decode(savedData)) do + _G[var] = val + end +end + function onLoad(savedData) if savedData and savedData ~= "" then - local loadedData = JSON.decode(savedData) - phaseId = loadedData.phaseId - broadcastChange = loadedData.broadcastChange + loadFromSaveTable(savedData) else phaseId = 1 broadcastChange = false @@ -81,13 +85,15 @@ function onLoad(savedData) click_function = 'changeState', function_owner = self, width = 600, - height = 600 + height = 600, + color = { r = 0, g = 0, b = 0, a = 0 } }) - self.addContextMenuItem("toggle broadcasting", updateBroadcast) + self.addContextMenuItem("Toggle Broadcasting", updateBroadcast) end -function updateBroadcast() +function updateBroadcast(playerColor) + Player[playerColor].clearSelectedObjects() for _, tracker in ipairs(getObjectsWithTag("LinkedPhaseTracker")) do tracker.setVar("broadcastChange", not broadcastChange) end diff --git a/unpacked/Custom_Tile Player Cards 2d30ee.ttslua b/unpacked/Custom_Tile Player Cards 2d30ee.ttslua index 2d3e78084..a8045506d 100644 --- a/unpacked/Custom_Tile Player Cards 2d30ee.ttslua +++ b/unpacked/Custom_Tile Player Cards 2d30ee.ttslua @@ -41,8 +41,133 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playercards/PlayerCardPanel") +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + end + + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + end + + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") + end + + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) + end + + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) + end + + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) + end + + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) + end + + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi +end end) __bundle_register("arkhamdb/ArkhamDb", function(require, _LOADED, __bundle_register, __bundle_modules) do @@ -60,7 +185,7 @@ do ---@class Request local Request = {} - -- Sets up the ArkhamDb interface. Should be called from the parent object on load. + -- Sets up the ArkhamDb interface. Should be called from the parent object on load. ArkhamDb.initialize = function() configuration = internal.getConfiguration() Request.start({ configuration.api_uri, configuration.taboo }, function(status) @@ -118,13 +243,14 @@ do local deck = Request.start(deckUri, function(status) if string.find(status.text, "") then - internal.maybePrint("Private deck ID " .. deckId .. " is not shared", playerColor) + internal.maybePrint("Private deck ID " .. deckId .. " is not shared.", playerColor) return false, "Private deck " .. deckId .. " is not shared" end - local json = JSON.decode(status.text) + + local json = JSON.decode(internal.fixUtf16String(status.text)) if not json then - internal.maybePrint("Deck ID " .. deckId .. " not found", playerColor) + internal.maybePrint("Deck ID " .. deckId .. " not found.", playerColor) return false, "Deck not found!" end @@ -224,8 +350,8 @@ do local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0 slots[RANDOM_WEAKNESS_ID] = nil - if randomWeaknessAmount ~= 0 then - for i=1, randomWeaknessAmount do + if randomWeaknessAmount > 0 then + for i = 1, randomWeaknessAmount do local weaknessId = allCardsBagApi.getRandomWeaknessId() slots[weaknessId] = (slots[weaknessId] or 0) + 1 end @@ -410,21 +536,23 @@ do local bondedList = { } for cardId, cardCount in pairs(slots) do local card = allCardsBagApi.getCardById(cardId) - if (card ~= nil and card.metadata.bonded ~= nil) then + if card ~= nil and card.metadata.bonded ~= nil then for _, bond in ipairs(card.metadata.bonded) do - -- add a bonded card for each copy of the parent card (except for Pendant of the Queen) - if bond.id == "06022" then - bondedCards[bond.id] = bond.count - else - bondedCards[bond.id] = bond.count * cardCount - end + -- 'unlimited' upper limit for cards without this data + local maxCount = bond.maxCount or 99 + + -- add a bonded card for each copy of the parent card (until the max value is reached) + bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount) + -- We need to know which cards are bonded to determine their position, remember them bondedList[bond.id] = true + -- Also adding taboo versions of bonded cards to the list bondedList[bond.id .. "-t"] = true end end end + -- Add any bonded cards to the main slots list for bondedId, bondedCount in pairs(bondedCards) do slots[bondedId] = bondedCount @@ -526,7 +654,6 @@ do ---@param uri table ---@param on_success fun(status, vararg): boolean, any ---@param on_error nil|fun(status, vararg): string - ---@vararg any ---@return Request function Request.start(uri, on_success, on_error, ...) local parameters = table.pack(...) @@ -541,7 +668,6 @@ do ---@param requests Request[] ---@param on_success fun(content: any, vararg: any) ---@param on_error fun(requests: Request, vararg: any)|nil - ---@vararg any function Request.with_all(requests, on_success, on_error, ...) local parameters = table.pack(...) @@ -587,243 +713,6 @@ do return ArkhamDb end end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local PlayAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") - end - - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") - end - - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") - end - - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) - end - - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) - end - - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) - end - - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) - end - - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) - end - - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) - end - - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) - end - - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) - end - - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) - end - - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi -end -end) -__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local AllCardsBagApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getAllCardsBag() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag") - end - - -- Returns a specific card from the bag, based on ArkhamDB ID - ---@param id table String ID of the card to retrieve - ---@return table table - -- If the indexes are still being constructed, an empty table is - -- returned. Otherwise, a single table with the following fields - -- cardData: TTS object data, suitable for spawning the card - -- cardMetadata: Table of parsed metadata - AllCardsBagApi.getCardById = function(id) - return getAllCardsBag().call("getCardById", {id = id}) - end - - -- Gets a random basic weakness from the bag. Once a given ID has been returned - -- it will be removed from the list and cannot be selected again until a reload - -- occurs or the indexes are rebuilt, which will refresh the list to include all - -- weaknesses. - ---@return string: ID of the selected weakness. - AllCardsBagApi.getRandomWeaknessId = function() - return getAllCardsBag().call("getRandomWeaknessId") - end - - AllCardsBagApi.isIndexReady = function() - return getAllCardsBag().call("isIndexReady") - end - - -- Called by Hotfix bags when they load. If we are still loading indexes, then - -- the all cards and hotfix bags are being loaded together, and we can ignore - -- this call as the hotfix will be included in the initial indexing. If it is - -- called once indexing is complete it means the hotfix bag has been added - -- later, and we should rebuild the index to integrate the hotfix bag. - AllCardsBagApi.rebuildIndexForHotfix = function() - return getAllCardsBag().call("rebuildIndexForHotfix") - end - - -- Searches the bag for cards which match the given name and returns a list. Note that this is - -- an O(n) search without index support. It may be slow. - ---@param name string or string fragment to search for names - ---@param exact boolean Whether the name match should be exact - AllCardsBagApi.getCardsByName = function(name, exact) - return getAllCardsBag().call("getCardsByName", {name = name, exact = exact}) - end - - AllCardsBagApi.isBagPresent = function() - return getAllCardsBag() and true - end - - -- Returns a list of cards from the bag matching a class and level (0 or upgraded) - ---@param class string class to retrieve ("Guardian", "Seeker", etc) - ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0 - ---@return table: If the indexes are still being constructed, returns an empty table. - -- Otherwise, a list of tables, each with the following fields - -- cardData: TTS object data, suitable for spawning the card - -- cardMetadata: Table of parsed metadata - AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) - return getAllCardsBag().call("getCardsByClassAndLevel", {class = class, upgraded = upgraded}) - end - - AllCardsBagApi.getCardsByCycle = function(cycle) - return getAllCardsBag().call("getCardsByCycle", cycle) - end - - AllCardsBagApi.getUniqueWeaknesses = function() - return getAllCardsBag().call("getUniqueWeaknesses") - end - - return AllCardsBagApi -end -end) __bundle_register("playercards/PlayerCardPanel", function(require, _LOADED, __bundle_register, __bundle_modules) ---@diagnostic disable: param-type-mismatch require("playercards/PlayerCardPanelData") @@ -832,6 +721,8 @@ local allCardsBagApi = require("playercards/AllCardsBagApi") local arkhamDb = require("arkhamdb/ArkhamDb") local spawnBag = require("playercards/SpawnBag") +local lastWeaknessTrait = "Madness" + -- Size and position information for the three rows of class buttons local CIRCLE_BUTTON_SIZE = 250 local CLASS_BUTTONS_X_OFFSET = 0.1325 @@ -853,22 +744,20 @@ local CYCLE_BUTTONS_Z_OFFSET = 0.2665 local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 } local TRANSPARENT = { 0, 0, 0, 0 } -local STARTER_DECK_MODE_STARTERS = "starters" -local STARTER_DECK_MODE_CARDS_ONLY = "cards" -local FACE_UP_ROTATION = { x = 0, y = 270, z = 0} -local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180} +local FACE_UP_ROTATION = { x = 0, y = 270, z = 0 } +local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180 } -- ---------- IMPORTANT ---------- -- Coordinates defined below are in global dimensions relative to the panel - DO NOT USE THESE --- DIRECTLY. Call scalePositions() before use, and reference the variables below +-- DIRECTLY. Call scalePositions() before use, and reference the variables below -- Layout width for a single card, in global coordinate space local CARD_WIDTH = 2.3 --- Coordinates to begin laying out cards. These vary based on the cards that are being placed by +-- Coordinates to begin laying out cards. These vary based on the cards that are being placed by -- considering the width of the cards, number of cards, and desired spread intervals. --- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y +-- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y -- coordinates on these provide global disances while the Z is local. local START_POSITIONS = { classCards = Vector(CARD_WIDTH * 9.5, 2, 1.4), @@ -877,7 +766,7 @@ local START_POSITIONS = { other = Vector(CARD_WIDTH * 9.5, 2, 1.4), randomWeakness = Vector(0, 2, 1.4), -- Because the card spread is handled by the SpawnBag, we don't know (programatically) where this - -- should be placed. If more customizable cards are added it will need to be moved. + -- should be placed. If more customizable cards are added it will need to be moved. summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7) } @@ -891,12 +780,12 @@ local INVESTIGATOR_POSITION_SHIFT_ROW = Vector(0, 0, 11) local INVESTIGATOR_POSITION_SHIFT_COL = Vector(-6, 0, 0) local INVESTIGATOR_MAX_COLS = 6 --- Positions relative to the minicard to place other stacks. Both signature card piles and starter +-- Positions relative to the minicard to place other stacks. Both signature card piles and starter -- decks use SIGNATURE_OFFSET local INVESTIGATOR_CARD_OFFSET = Vector(0, 0, 2.55) local INVESTIGATOR_SIGNATURE_OFFSET = Vector(0, 0, 5.75) --- USE THESE! Positions and offset shifts accounting for the scale of the panel +-- USE THESE! Positions and offset shifts accounting for the scale of the panel local startPositions local cardRowOffset local cardGroupOffset @@ -905,7 +794,14 @@ local investigatorPositionShiftCol local investigatorCardOffset local investigatorSignatureOffset -local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" } +local CLASS_LIST = { + "Guardian", + "Seeker", + "Rogue", + "Mystic", + "Survivor", + "Neutral" +} local CYCLE_LIST = { "Core", "The Dunwich Legacy", @@ -921,22 +817,21 @@ local CYCLE_LIST = { } local excludedNonBasicWeaknesses - -local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY -local helpVisibleToPlayers = { } +local spawnStarterDecks = false +local helpVisibleToPlayers = {} function onSave() return JSON.encode({ spawnBagState = spawnBag.getStateForSave() }) end function onLoad(savedData) - arkhamDb.initialize() - if (savedData ~= nil) then - local saveState = JSON.decode(savedData) or { } - if (saveState.spawnBagState ~= nil) then + if savedData and savedData ~= "" then + local saveState = JSON.decode(savedData) or {} + if saveState.spawnBagState ~= nil then spawnBag.loadFromSave(saveState.spawnBagState) end end + arkhamDb.initialize() buildExcludedWeaknessList() createButtons() end @@ -944,7 +839,7 @@ end -- Build a list of non-basic weaknesses which should be excluded from the last weakness set, -- including all signature cards and evolved weaknesses. function buildExcludedWeaknessList() - excludedNonBasicWeaknesses = { } + excludedNonBasicWeaknesses = {} for _, investigator in pairs(INVESTIGATORS) do for _, signatureId in ipairs(investigator.signatures) do excludedNonBasicWeaknesses[signatureId] = true @@ -1047,9 +942,10 @@ function createWeaknessButtons() weaknessButtonParams.tooltip = "All Weaknesses" weaknessButtonParams.position = buttonPos self.createButton(weaknessButtonParams) + buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET weaknessButtonParams.click_function = "spawnRandomWeakness" - weaknessButtonParams.tooltip = "Random Basic Weakness" + weaknessButtonParams.tooltip = "Random Basic Weakness\nRight-click to specify a trait" weaknessButtonParams.position = buttonPos self.createButton(weaknessButtonParams) end @@ -1101,9 +997,8 @@ function createCycleButtons() if rowCount == 3 then -- Account for two centered buttons on the final row buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2 - --[[ Account for centered button on the final row - buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET - ]] + -- Account for centered button on the final row + -- buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET end else buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET @@ -1124,8 +1019,6 @@ function createClearButton() end function createInvestigatorModeButtons() - local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS - self.createButton({ function_owner = self, click_function = "setCardsOnlyMode", @@ -1133,18 +1026,18 @@ function createInvestigatorModeButtons() height = 170, width = 760, scale = Vector(0.25, 1, 0.25), - color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR + color = spawnStarterDecks and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR }) self.createButton({ function_owner = self, - click_function = "setStarterDeckMode", + click_function = "setspawnStarterDecks", position = Vector(0.66, 0.1, -0.322), height = 170, width = 760, scale = Vector(0.25, 1, 0.25), - color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT + color = spawnStarterDecks and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT }) - local checkX = starterMode and 0.52 or 0.11 + local checkX = spawnStarterDecks and 0.52 or 0.11 self.createButton({ function_owner = self, label = "✓", @@ -1152,12 +1045,77 @@ function createInvestigatorModeButtons() position = Vector(checkX, 0.11, -0.317), height = 0, width = 0, - scale = Vector(0.3, 1, 0.3), + font_size = 300, + scale = Vector(0.1, 1, 0.1), font_color = { 0, 0, 0 }, color = { 1, 1, 1 } }) end +function createXML(showOtherCardsButton) + -- basic XML for the help button + local xmlTable = { + { + tag = "Panel", + attributes = { + active = "false", + id = "helpPanel", + position = "-165 -70 -2", + rotation = "0 0 180", + height = "50", + width = "107", + color = "#00000099" + }, + children = { + tag = "Text", + attributes = { + id = "helpText", + rectAlignment = "MiddleCenter", + height = "480", + width = "1000", + scale = "0.1 0.1 1", + fontSize = "66", + color = "#F5F5F5", + backgroundColor = "#FF0000", + alignment = "MiddleLeft", + horizontalOverflow = "wrap", + text = "• Select a group to place cards\n" .. + "• Copy the cards you want for your deck\n" .. + "• Select a new group to clear the placed cards and see new ones\n" .. + "• Clear to remove all cards" + } + } + } + } + + -- add the "Additional Cards" button if cards without cycle were detected + if showOtherCardsButton then + local otherCardsButtonXml = { + tag = "Panel", + attributes = { + position = "44.25 65.75 -11", + rotation = "0 0 180", + height = "225", + width = "225", + scale = "0.1 0.1 1", + onClick = "spawnOtherCards" + }, + children = { + tag = "Image", + attributes = { image = "OtherCards" } + } + } + table.insert(xmlTable, otherCardsButtonXml) + end + helpVisibleToPlayers = {} + self.UI.setXmlTable(xmlTable) +end + +-- click function for the XML button for the additional player cards +function spawnOtherCards() + spawnCycle("Other") +end + function toggleHelp(_, playerColor, _) if helpVisibleToPlayers[playerColor] then helpVisibleToPlayers[playerColor] = nil @@ -1181,13 +1139,13 @@ function updateHelpVisibility() self.UI.setAttribute("helpPanel", "active", string.len(visibility) > 0) end -function setStarterDeckMode() - starterDeckMode = STARTER_DECK_MODE_STARTERS +function setspawnStarterDecks() + spawnStarterDecks = true updateStarterModeButtons() end function setCardsOnlyMode() - starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY + spawnStarterDecks = false updateStarterModeButtons() end @@ -1211,7 +1169,7 @@ end function scalePositions() -- Assume scaling is consistent in X and Z dimensions local scale = 1 / self.getScale().x - startPositions = { } + startPositions = {} for key, pos in pairs(START_POSITIONS) do -- Because a scaled object means a different global size, using global distance for Z results in -- the cards being closer or farther depending on the scale. Leave the Z values and only scale X and Y @@ -1232,14 +1190,12 @@ function deleteAll() spawnBag.recall(true) end --- Spawn an investigator group, based on the current UI setting for either investigators or starter --- decks. +-- Spawn an investigator group, based on the current UI setting for either investigators or starter decks ---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData function spawnInvestigatorGroup(groupName) - local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS prepareToPlaceCards() Wait.frames(function() - if starterMode then + if spawnStarterDecks then spawnStarters(groupName) else spawnInvestigators(groupName) @@ -1247,12 +1203,12 @@ function spawnInvestigatorGroup(groupName) end, 2) end --- Spawn cards for all investigators in the given group. This creates piles for all defined +-- Spawn cards for all investigators in the given group. This creates piles for all defined -- investigator cards and minicards as well as the signature cards. ---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData function spawnInvestigators(groupName) if INVESTIGATOR_GROUPS[groupName] == nil then - printToAll("No " .. groupName .. " data yet") + printToAll("No investigator data for " .. groupName .. " yet") return end @@ -1261,7 +1217,7 @@ function spawnInvestigators(groupName) local investigatorCount = #INVESTIGATOR_GROUPS[groupName] local position = getInvestigatorRowStartPos(investigatorCount, row) - for i, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do + for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do spawnBag.spawn(spawnSpec) end @@ -1278,14 +1234,14 @@ end function getInvestigatorRowStartPos(investigatorCount, row) local rowStart = Vector(startPositions.investigator) rowStart:add(Vector( - investigatorPositionShiftRow.x * (row - 1), - investigatorPositionShiftRow.y * (row - 1), - investigatorPositionShiftRow.z * (row - 1))) + investigatorPositionShiftRow.x * (row - 1), + investigatorPositionShiftRow.y * (row - 1), + investigatorPositionShiftRow.z * (row - 1))) local investigatorsInRow = math.min(investigatorCount - INVESTIGATOR_MAX_COLS * (row - 1), INVESTIGATOR_MAX_COLS) rowStart:add(Vector( - investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, - investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, - investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2)) + investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, + investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2, + investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2)) return rowStart end @@ -1297,23 +1253,23 @@ function buildInvestigatorSpawnSpec(investigatorName, investigatorData, position local sigPos = Vector(position):add(investigatorSignatureOffset) local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position) table.insert(spawns, { - name = investigatorName .. "signatures", - cards = investigatorData.signatures, - globalPos = self.positionToWorld(sigPos), - rotation = FACE_UP_ROTATION - }) + name = investigatorName .. "signatures", + cards = investigatorData.signatures, + globalPos = self.positionToWorld(sigPos), + rotation = FACE_UP_ROTATION + }) return spawns end --- Builds the spawn specs for minicards and investigator cards. These are common enough to be +-- Builds the spawn specs for minicards and investigator cards. These are common enough to be -- shared, and will only differ in whether they spawn the full stack of possible investigator and -- minicards, or only the first of each. ---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData ---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS ---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below ---@param oneCardOnly? boolean If true, will spawn only the first card in the investigator card ---- and minicard lists. Otherwise, spawn them all in a deck +--- and minicard lists. Otherwise, spawn them all in a deck function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly) local cardPos = Vector(position):add(investigatorCardOffset) return { @@ -1360,23 +1316,24 @@ function spawnStarterDeck(investigatorName, investigatorData, position) end local deckPos = Vector(position):add(investigatorSignatureOffset) arkhamDb.getDecklist("None", investigatorData.starterDeck, true, false, false, function(slots) - local cardIdList = { } + local cardIdList = {} for id, count in pairs(slots) do for i = 1, count do table.insert(cardIdList, id) end end spawnBag.spawn({ - name = investigatorName.."starter", + name = investigatorName .. "starter", cards = cardIdList, globalPos = self.positionToWorld(deckPos), rotation = FACE_DOWN_ROTATION }) end) end + -- Clears the currently placed cards, then places cards for the given class and level spread ---@param cardClass string Class to place ("Guardian", "Seeker", etc) ----@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. +---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. function spawnClassCards(cardClass, isUpgraded) prepareToPlaceCards() Wait.frames(function() placeClassCards(cardClass, isUpgraded) end, 2) @@ -1384,18 +1341,15 @@ end -- Spawn the class cards. ---@param cardClass string Class to place ("Guardian", "Seeker", etc) ----@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. +---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0. function placeClassCards(cardClass, isUpgraded) - local indexReady = allCardsBagApi.isIndexReady() - if (not indexReady) then - broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2}) - return - end + if not allCardsBagApi.isIndexReady() then return end + local cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded) - local skillList = { } - local eventList = { } - local assetList = { } + local skillList = {} + local eventList = {} + local assetList = {} for _, cardId in ipairs(cardIdList) do local cardMetadata = allCardsBagApi.getCardById(cardId).metadata if (cardMetadata.type == "Skill") then @@ -1444,21 +1398,20 @@ end -- Spawns the investigator sets and all cards for the given cycle ---@param cycle string Name of a cycle, should match the standard used in card metadata function spawnCycle(cycle) + if not allCardsBagApi.isIndexReady() then return end + prepareToPlaceCards() spawnInvestigators(cycle) - local indexReady = allCardsBagApi.isIndexReady() - if (not indexReady) then - broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2}) - return - end - local cycleCardList = allCardsBagApi.getCardsByCycle(cycle) - local copiedList = { } - for i, id in ipairs(cycleCardList) do - copiedList[i] = id + + -- sort custom cards + local sortByMetadata = false + if cycle == "Other" then + sortByMetadata = true end + spawnBag.spawn({ - name = "cycle"..cycle, - cards = copiedList, + name = "cycle" .. cycle, + cards = allCardsBagApi.getCardsByCycle(cycle, sortByMetadata), globalPos = self.positionToWorld(startPositions.cycle), rotation = FACE_UP_ROTATION, spread = true, @@ -1498,16 +1451,13 @@ end -- Clears the current cards, and places all basic weaknesses on the table. function spawnWeaknesses() + if not allCardsBagApi.isIndexReady() then return end + prepareToPlaceCards() - local indexReady = allCardsBagApi.isIndexReady() - if (not indexReady) then - broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2}) - return - end - local weaknessIdList = allCardsBagApi.getUniqueWeaknesses() - local basicWeaknessList = { } - local otherWeaknessList = { } - for i, id in ipairs(weaknessIdList) do + + local basicWeaknessList = {} + local otherWeaknessList = {} + for _, id in ipairs(allCardsBagApi.getUniqueWeaknesses()) do local cardMetadata = allCardsBagApi.getCardById(id).metadata if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount > 0 then table.insert(basicWeaknessList, id) @@ -1544,13 +1494,32 @@ function spawnWeaknesses() }) end -function spawnRandomWeakness() +function spawnRandomWeakness(_, playerColor, isRightClick) prepareToPlaceCards() - local weaknessId = allCardsBagApi.getRandomWeaknessId() - if (weaknessId == nil) then - broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2}) - return + + if not isRightClick then + local weaknessId = allCardsBagApi.getRandomWeaknessId() + if weaknessId == nil then + broadcastToAll("All basic weaknesses are in play!", { 0.9, 0.2, 0.2 }) + else + spawnSingleWeakness(weaknessId) + end + else + Player[playerColor].showInputDialog("Specify a trait for the weakness (split multiple eligible traits with '|'):", lastWeaknessTrait, + function(text) + lastWeaknessTrait = text + local availableWeaknesses = allCardsBagApi.buildAvailableWeaknesses(text) + if #availableWeaknesses > 0 then + spawnSingleWeakness(availableWeaknesses[math.random(#availableWeaknesses)]) + else + broadcastToAll("No matching weakness available!", { 0.9, 0.2, 0.2 }) + end + end) end +end + +-- spawn the random weakness +function spawnSingleWeakness(weaknessId) spawnBag.spawn({ name = "randomWeakness", cards = { weaknessId }, @@ -1559,57 +1528,155 @@ function spawnRandomWeakness() }) end end) +__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local AllCardsBagApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getAllCardsBag() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag") + end + + -- internal function to create a copy of the table to avoid operating on variables owned by different objects + local function returnCopyOfList(data) + local copiedList = {} + for _, id in ipairs(data) do + table.insert(copiedList, id) + end + return copiedList + end + + -- Returns a specific card from the bag, based on ArkhamDB ID + ---@param id string ID of the card to retrieve + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a single table with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardById = function(id) + return getAllCardsBag().call("getCardById", { id = id }) + end + + -- Gets a random basic weakness from the bag. Once a given ID has been returned it + -- will be removed from the list and cannot be selected again until a reload occurs + -- or the indexes are rebuilt, which will refresh the list to include all weaknesses. + ---@return string: ID of the selected weakness + AllCardsBagApi.getRandomWeaknessId = function() + return getAllCardsBag().call("getRandomWeaknessId") + end + + AllCardsBagApi.isIndexReady = function() + return getAllCardsBag().call("isIndexReady") + end + + -- Called by Hotfix bags when they load. If we are still loading indexes, then + -- the all cards and hotfix bags are being loaded together, and we can ignore + -- this call as the hotfix will be included in the initial indexing. If it is + -- called once indexing is complete it means the hotfix bag has been added + -- later, and we should rebuild the index to integrate the hotfix bag. + AllCardsBagApi.rebuildIndexForHotfix = function() + getAllCardsBag().call("rebuildIndexForHotfix") + end + + -- Searches the bag for cards which match the given name and returns a list. + -- Note that this is an O(n) search without index support. It may be slow. + ---@param name string or string fragment to search for names + ---@param exact boolean Whether the name match should be exact + AllCardsBagApi.getCardsByName = function(name, exact) + return returnCopyOfList(getAllCardsBag().call("getCardsByName", { name = name, exact = exact })) + end + + AllCardsBagApi.isBagPresent = function() + return getAllCardsBag() and true + end + + -- Returns a list of cards from the bag matching a class and level (0 or upgraded) + ---@param class string class to retrieve ("Guardian", "Seeker", etc) + ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0 + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a list of tables, each with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) + return returnCopyOfList(getAllCardsBag().call("getCardsByClassAndLevel", { class = class, upgraded = upgraded })) + end + + -- Returns a list of cards from the bag matching a cycle + ---@param cycle string Cycle to retrieve ("The Scarlet Keys" etc.) + ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a list of tables, each with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata) + return returnCopyOfList(getAllCardsBag().call("getCardsByCycle", { cycle = cycle, sortByMetadata = sortByMetadata })) + end + + -- Constructs a list of available basic weaknesses by starting with the full pool of basic + -- weaknesses then removing any which are currently in the play or deck construction areas + ---@param traits? string Trait(s) to use as filter + ---@return table: Array of weakness IDs which are valid to choose from + AllCardsBagApi.buildAvailableWeaknesses = function(traits) + return returnCopyOfList(getAllCardsBag().call("buildAvailableWeaknesses", traits)) + end + + AllCardsBagApi.getUniqueWeaknesses = function() + return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses")) + end + + return AllCardsBagApi +end +end) __bundle_register("playercards/PlayerCardPanelData", function(require, _LOADED, __bundle_register, __bundle_modules) BONDED_CARD_LIST = { - "05314", -- Soothing Melody - "06277", -- Wish Eater - "06019", -- Bloodlust - "06022", -- Pendant of the Queen - "05317", -- Blood-rite - "06113", -- Essence of the Dream - "06028", -- Stars Are Right - "06025", -- Guardian of the Crystallizer - "06283", -- Unbound Beast - "06032", -- Zeal - "06031", -- Hope - "06033", -- Augur - "06331", -- Dream Parasite - "06015a", -- Dream-Gate - "10006", -- Aetheric Current (Yuggoth) - "10007", -- Aetheric Current (Yoth) - "10036", -- Blade of Yoth - "10039", -- Evanescent Ascension - "10045", -- Uncanny Growth - "10063", -- Bianca - "10086", -- Rot - "10087", -- Rot - "10088", -- Rot - "10089", -- Rot - "10090", -- Rot - "10106", -- Keeper of the Key - "10107", -- Servant of Brass - "10134", -- Twilight Diadem + "05314", -- Soothing Melody + "06277", -- Wish Eater + "06019", -- Bloodlust + "06022", -- Pendant of the Queen + "05317", -- Blood-rite + "06113", -- Essence of the Dream + "06028", -- Stars Are Right + "06025", -- Guardian of the Crystallizer + "06283", -- Unbound Beast + "06032", -- Zeal + "06031", -- Hope + "06033", -- Augur + "06331", -- Dream Parasite + "06015a", -- Dream-Gate + "10006", -- Aetheric Current (Yuggoth) + "10007", -- Aetheric Current (Yoth) + "10036", -- Blade of Yoth + "10039", -- Evanescent Ascension + "10045", -- Uncanny Growth + "10063", -- Bianca + "10086", -- Rot + "10087", -- Rot + "10088", -- Rot + "10089", -- Rot + "10090", -- Rot + "10106", -- Keeper of the Key + "10107", -- Servant of Brass + "10134", -- Twilight Diadem } UPGRADE_SHEET_LIST = { - "09040-c", -- Alchemical Distillation - "09023-c", -- Custom Modifications - "09059-c", -- Damning Testimony - "09041-c", -- Emperical Hypothesis - "09060-c", -- Friends in Low Places - "09101-c", -- Grizzled - "09061-c", -- Honed Instinct - "09021-c", -- Hunter's Armor - "09119-c", -- Hyperphysical Shotcaster - "09079-c", -- Living Ink - "09100-c", -- Makeshift Trap - "09099-c", -- Pocket Multi Tool - "09081-c", -- Power Word + "09040-c", -- Alchemical Distillation + "09023-c", -- Custom Modifications + "09059-c", -- Damning Testimony + "09041-c", -- Emperical Hypothesis + "09060-c", -- Friends in Low Places + "09101-c", -- Grizzled + "09061-c", -- Honed Instinct + "09021-c", -- Hunter's Armor + "09119-c", -- Hyperphysical Shotcaster + "09079-c", -- Living Ink + "09100-c", -- Makeshift Trap + "09099-c", -- Pocket Multi Tool + "09081-c", -- Power Word "09081-t-c", -- Power Word (Taboo) - "09022-c", -- Runic Axe + "09022-c", -- Runic Axe "09022-t-c", -- Runic Axe (Taboo) - "09080-c", -- Summoned Servitor - "09042-c", -- Raven's Quill + "09080-c", -- Summoned Servitor + "09042-c", -- Raven's Quill } EVOLVED_WEAKNESSES = { @@ -1622,152 +1689,152 @@ EVOLVED_WEAKNESSES = { ------------------ START INVESTIGATOR DATA DEFINITION ------------------ INVESTIGATOR_GROUPS = { - ["Guardian"] = { - "Roland Banks", - "Zoey Samaras", - "Mark Harrigan", - "Leo Anderson", - "Carolyn Fern", - "Tommy Muldoon", - "Nathaniel Cho", - "Sister Mary", - "Daniela Reyes", - "Carson Sinclair", + ["Guardian"] = { + "Roland Banks", + "Zoey Samaras", + "Mark Harrigan", + "Leo Anderson", + "Carolyn Fern", + "Tommy Muldoon", + "Nathaniel Cho", + "Sister Mary", + "Daniela Reyes", + "Carson Sinclair", "Wilson Richards" - }, - ["Seeker"] = { - "Daisy Walker", - "Rex Murphy", - "Minh Thi Phan", - "Ursula Downs", - "Joe Diamond", - "Mandy Thompson", - "Harvey Walters", - "Amanda Sharpe", - "Norman Withers", - "Vincent Lee", + }, + ["Seeker"] = { + "Daisy Walker", + "Rex Murphy", + "Minh Thi Phan", + "Ursula Downs", + "Joe Diamond", + "Mandy Thompson", + "Harvey Walters", + "Amanda Sharpe", + "Norman Withers", + "Vincent Lee", "Kate Winthrop" - }, - ["Rogue"] = { - "\"Skids\" O'Toole", - "Jenny Barnes", - "Sefina Rousseau", - "Finn Edwards", - "Preston Fairmont", - "Tony Morgan", - "Winifred Habbamock", - "Trish Scarborough", - "Monterey Jack", - "Kymani Jones", + }, + ["Rogue"] = { + "\"Skids\" O'Toole", + "Jenny Barnes", + "Sefina Rousseau", + "Finn Edwards", + "Preston Fairmont", + "Tony Morgan", + "Winifred Habbamock", + "Trish Scarborough", + "Monterey Jack", + "Kymani Jones", "Alessandra Zorzi" - }, - ["Mystic"] = { - "Agnes Baker", - "Jim Culver", - "Akachi Onyele", - "Father Mateo", - "Diana Stanley", - "Marie Lambeau", - "Luke Robinson", - "Jacqueline Fine", - "Dexter Drake", - "Lily Chen", - "Amina Zidane", - "Gloria Goldberg", + }, + ["Mystic"] = { + "Agnes Baker", + "Jim Culver", + "Akachi Onyele", + "Father Mateo", + "Diana Stanley", + "Marie Lambeau", + "Luke Robinson", + "Jacqueline Fine", + "Dexter Drake", + "Lily Chen", + "Amina Zidane", + "Gloria Goldberg", "Kōhaku Narukami" - }, - ["Survivor"] = { - "Wendy Adams", - "\"Ashcan\" Pete", - "William Yorick", - "Calvin Wright", - "Rita Young", - "Patrice Hathaway", - "Stella Clark", - "Silas Marsh", - "Bob Jenkins", - "Darrell Simmons", + }, + ["Survivor"] = { + "Wendy Adams", + "\"Ashcan\" Pete", + "William Yorick", + "Calvin Wright", + "Rita Young", + "Patrice Hathaway", + "Stella Clark", + "Silas Marsh", + "Bob Jenkins", + "Darrell Simmons", "Hank Samson" - }, - ["Neutral"] = { - "Lola Hayes", - "Charlie Kane", - "Subject 5U-21" - }, - ["Core"] = { - "Roland Banks", - "Daisy Walker", - "\"Skids\" O'Toole", - "Agnes Baker", - "Wendy Adams" - }, - ["The Dunwich Legacy"] = { - "Zoey Samaras", - "Rex Murphy", - "Jenny Barnes", - "Jim Culver", - "\"Ashcan\" Pete" - }, - ["The Path to Carcosa"] = { - "Mark Harrigan", - "Minh Thi Phan", - "Sefina Rousseau", - "Akachi Onyele", - "William Yorick", - "Lola Hayes" - }, - ["The Forgotten Age"] = { - "Leo Anderson", - "Ursula Downs", - "Finn Edwards", - "Father Mateo", - "Calvin Wright" - }, - ["The Circle Undone"] = { - "Carolyn Fern", - "Joe Diamond", - "Preston Fairmont", - "Diana Stanley", - "Rita Young", - "Marie Lambeau" - }, - ["The Dream-Eaters"] = { - "Tommy Muldoon", - "Mandy Thompson", - "Tony Morgan", - "Luke Robinson", - "Patrice Hathaway" - }, - ["Investigator Packs"] = { - "Nathaniel Cho", - "Harvey Walters", - "Winifred Habbamock", - "Jacqueline Fine", - "Stella Clark", - "Gloria Goldberg" - }, - ["The Innsmouth Conspiracy"] = { - "Sister Mary", - "Amanda Sharpe", - "Trish Scarborough", - "Dexter Drake", - "Silas Marsh" - }, - ["Edge of the Earth"] = { - "Daniela Reyes", - "Norman Withers", - "Monterey Jack", - "Lily Chen", - "Bob Jenkins" - }, - ["The Scarlet Keys"] = { - "Carson Sinclair", - "Vincent Lee", - "Kymani Jones", - "Amina Zidane", - "Darrell Simmons", - "Charlie Kane" - }, + }, + ["Neutral"] = { + "Lola Hayes", + "Charlie Kane", + "Subject 5U-21" + }, + ["Core"] = { + "Roland Banks", + "Daisy Walker", + "\"Skids\" O'Toole", + "Agnes Baker", + "Wendy Adams" + }, + ["The Dunwich Legacy"] = { + "Zoey Samaras", + "Rex Murphy", + "Jenny Barnes", + "Jim Culver", + "\"Ashcan\" Pete" + }, + ["The Path to Carcosa"] = { + "Mark Harrigan", + "Minh Thi Phan", + "Sefina Rousseau", + "Akachi Onyele", + "William Yorick", + "Lola Hayes" + }, + ["The Forgotten Age"] = { + "Leo Anderson", + "Ursula Downs", + "Finn Edwards", + "Father Mateo", + "Calvin Wright" + }, + ["The Circle Undone"] = { + "Carolyn Fern", + "Joe Diamond", + "Preston Fairmont", + "Diana Stanley", + "Rita Young", + "Marie Lambeau" + }, + ["The Dream-Eaters"] = { + "Tommy Muldoon", + "Mandy Thompson", + "Tony Morgan", + "Luke Robinson", + "Patrice Hathaway" + }, + ["Investigator Packs"] = { + "Nathaniel Cho", + "Harvey Walters", + "Winifred Habbamock", + "Jacqueline Fine", + "Stella Clark", + "Gloria Goldberg" + }, + ["The Innsmouth Conspiracy"] = { + "Sister Mary", + "Amanda Sharpe", + "Trish Scarborough", + "Dexter Drake", + "Silas Marsh" + }, + ["Edge of the Earth"] = { + "Daniela Reyes", + "Norman Withers", + "Monterey Jack", + "Lily Chen", + "Bob Jenkins" + }, + ["The Scarlet Keys"] = { + "Carson Sinclair", + "Vincent Lee", + "Kymani Jones", + "Amina Zidane", + "Darrell Simmons", + "Charlie Kane" + }, ["The Feast of Hemlock Vale"] = { "Wilson Richards", "Kate Winthrop", @@ -1817,9 +1884,9 @@ INVESTIGATORS["Zoey Samaras"] = { starterDeck = "2624950" } INVESTIGATORS["Rex Murphy"] = { - cards = { "02002", "02002-t" }, + cards = { "02002", "02002-t", "02002-p", "02002-pf", "02002-pb" }, minicards = { "02002-m" }, - signatures = { "02008", "02009" }, + signatures = { "02008", "02009", "90079", "90080" }, starterDeck = "2624958" } INVESTIGATORS["Jenny Barnes"] = { @@ -2111,38 +2178,38 @@ INVESTIGATORS["Wilson Richards"] = { cards = { "10001" }, minicards = { "10001-m" }, signatures = { "10002", "10003" }, - starterDeck = "2634667" --carson deck as placeholder + starterDeck = "3893753" } INVESTIGATORS["Kate Winthrop"] = { cards = { "10004" }, minicards = { "10004-m" }, signatures = { "10005", "10006", "10007", "10008" }, - starterDeck = "2643928" --harvey deck as placeholder + starterDeck = "3893779" } INVESTIGATORS["Alessandra Zorzi"] = { cards = { "10009" }, minicards = { "10009-m" }, signatures = { "10010", "10010", "10010", "10011" }, - starterDeck = "2643931" --winifred deck as placeholder + starterDeck = "3893775" } INVESTIGATORS["Kōhaku Narukami"] = { cards = { "10012" }, minicards = { "10012-m" }, signatures = { "10013", "10014" }, - starterDeck = "2636199" --gloria deck as placeholder + starterDeck = "3893763" } INVESTIGATORS["Hank Samson"] = { cards = { "10015", "10015-b1", "10015-b2" }, minicards = { "10015-m" }, - signatures = { "10017", "10018"}, - starterDeck = "2643934" --stella deck as placeholder + signatures = { "10017", "10018" }, + starterDeck = "3893788" } -- PnP content INVESTIGATORS["Subject 5U-21"] = { cards = { "89001" }, minicards = { "89001-m" }, signatures = { "89002", "89003", "89003", "89003", "89004", "89004", "89004", "89005" }, - starterDeck = "2624990" -- Lola's deck id until Suzi is on ArkhamDB + starterDeck = "3893795" } -- Promo content INVESTIGATORS["Gloria Goldberg"] = { @@ -2153,6 +2220,294 @@ INVESTIGATORS["Gloria Goldberg"] = { } ------------------ END INVESTIGATOR DATA DEFINITION ------------------ end) +__bundle_register("playercards/SpawnBag", function(require, _LOADED, __bundle_register, __bundle_modules) +require("playercards/PlayerCardSpawner") + +-- Allows spawning of defined lists of cards which will be created from the template in the All +-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while +-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the +-- spawned objects. Objects moved out of this area will not be cleaned up. +-- +-- SpawnSpec: Spawning requires a spawn specification with the following structure: +-- { +-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned, +-- but each requires a separate name +-- cards: A list of card IDs to be spawned +-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be +-- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 } +-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with +-- globalPos, this should be a valid Vector with x, y, and z defined +-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a +-- spread moving to the right. globalPos will define the location of the first card, each +-- after that will be moved a predefined distance +-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be +-- laid out in before starting a new row. If spread is true but spreadCols is not set, all +-- cards will be in a single row (however long that may be) +-- } +-- See BondedBag.ttslua for an example +do + local allCardsBagApi = require("playercards/AllCardsBagApi") + + local SpawnBag = {} + local internal = {} + + -- To assist debugging, will draw a box around the recall zone when it's set up + local SHOW_RECALL_ZONE = false + + -- Distance to expand the recall zone around any added object. + local RECALL_BUFFER_X = 0.9 + local RECALL_BUFFER_Z = 0.5 + + -- In order to mimic the behavior of the previous memory buttons we use a temporary bag when + -- recalling objects. This bag is tiny and transparent, and will be placed at the same location as + -- this object. Once all placed cards are recalled bag to this bag, it will be destroyed + local RECALL_BAG = { + Name = "Bag", + Transform = { + scaleX = 0.01, + scaleY = 0.01, + scaleZ = 0.01, + }, + ColorDiffuse = { + r = 0, + g = 0, + b = 0, + a = 0, + }, + Locked = true, + Grid = true, + Snap = false, + Tooltip = false + } + + -- Tracks what has been placed by this "bag" so they can be recalled + local placedSpecs = {} + local placedObjectGuids = {} + local recallZone = nil + + -- Loads a table of saved state, extracted during the parent object's onLoad + SpawnBag.loadFromSave = function(saveTable) + placedSpecs = saveTable.placed + placedObjectGuids = saveTable.placedObjects + recallZone = saveTable.recall + end + + -- Generates a table of save state that can be included in the parent object's onSave + SpawnBag.getStateForSave = function() + return { + placed = placedSpecs, + placedObjects = placedObjectGuids, + recall = recallZone, + } + end + + -- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples + SpawnBag.spawn = function(spawnSpec) + -- Limit to one placement at a time + if placedSpecs[spawnSpec.name] or spawnSpec == nil then return end + + local cardsToSpawn = {} + for _, cardId in ipairs(spawnSpec.cards) do + local card = allCardsBagApi.getCardById(cardId) + if card ~= nil then + table.insert(cardsToSpawn, card) + end + end + if spawnSpec.spread then + Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject) + else + -- TTS decks come out in reverse order of the cards, reverse the list so the input order stays + -- This only applies for decks; spreads are spawned by us in the order given + if spawnSpec.rotation.z ~= 180 then + cardsToSpawn = internal.reverseList(cardsToSpawn) + end + Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, internal.recordPlacedObject) + end + placedSpecs[spawnSpec.name] = true + end + + -- Recalls all spawned objects to the bag, and clears the placedObjectGuids list + ---@param fast boolean If true, cards will be deleted directly without faking the bag recall. + SpawnBag.recall = function(fast) + if fast then + internal.deleteSpawned() + else + internal.recallSpawned() + end + + -- We've recalled everything we can, some cards may have been moved out of the card area. Just reset at this point. + placedSpecs = {} + placedObjectGuids = {} + recallZone = nil + end + + -- Delete all spawned cards + internal.deleteSpawned = function() + for guid, _ in pairs(placedObjectGuids) do + local obj = getObjectFromGUID(guid) + if (obj ~= nil) then + if (internal.isInRecallZone(obj)) then + obj.destruct() + end + placedObjectGuids[guid] = nil + end + end + end + + -- Recalls spawned cards with a fake bag that replicates the memory bag recall style + internal.recallSpawned = function() + local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() }) + for guid, _ in pairs(placedObjectGuids) do + local obj = getObjectFromGUID(guid) + if (obj ~= nil) then + if (internal.isInRecallZone(obj)) then + trash.putObject(obj) + end + placedObjectGuids[guid] = nil + end + end + + trash.destruct() + end + + -- Callback for when an object has been spawned. Tracks the object for later recall and updates the recall zone. + internal.recordPlacedObject = function(spawned) + placedObjectGuids[spawned.getGUID()] = true + internal.expandRecallZone(spawned) + end + + -- Expands the current recall zone based on the position of the given object. The recall zone will + -- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer + internal.expandRecallZone = function(spawnedCard) + local pos = spawnedCard.getPosition() + if (recallZone == nil) then + -- First card out of the bag, initialize surrounding that + recallZone = {} + recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z } + recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z } + return + end + + if pos.x > recallZone.upperLeft.x then + recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X + end + if pos.x < recallZone.lowerRight.x then + recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X + end + if pos.z > recallZone.upperLeft.z then + recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z + end + if pos.z < recallZone.lowerRight.z then + recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z + end + + if SHOW_RECALL_ZONE then + local y = 1.5 + local thick = 0.05 + Global.setVectorLines({ + { + points = { { recallZone.upperLeft.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.lowerRight.z } }, + color = { 1, 0, 0 }, + thickness = thick, + rotation = { 0, 0, 0 } + }, + { + points = { { recallZone.upperLeft.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.lowerRight.z } }, + color = { 1, 0, 0 }, + thickness = thick, + rotation = { 0, 0, 0 } + }, + { + points = { { recallZone.lowerRight.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.upperLeft.z } }, + color = { 1, 0, 0 }, + thickness = thick, + rotation = { 0, 0, 0 } + }, + { + points = { { recallZone.lowerRight.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.upperLeft.z } }, + color = { 1, 0, 0 }, + thickness = thick, + rotation = { 0, 0, 0 } + } + }) + end + end + + -- Checks to see if the given object is in the current recall zone. If there isn't a recall zone, + -- will return true so that everything can be easily cleaned up. + internal.isInRecallZone = function(obj) + if (recallZone == nil) then + return true + end + local pos = obj.getPosition() + return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x + and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z) + end + + internal.reverseList = function(list) + local reversed = {} + for i = 1, #list do + reversed[i] = list[#list - i + 1] + end + + return reversed + end + + return SpawnBag +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) __bundle_register("playercards/PlayerCardSpawner", function(require, _LOADED, __bundle_register, __bundle_modules) -- Amount to shift for the next card (zShift) or next row of cards (xShift) -- Note that the table rotation is weird, and the X axis is vertical while the @@ -2162,8 +2517,8 @@ local SPREAD_X_SHIFT = -3.66 Spawner = { } --- Spawns a list of cards at the given position/rotation. This will separate cards by size - --- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If +-- Spawns a list of cards at the given position/rotation. This will separate cards by size - +-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If -- there are different types, the provided callback will be called once for each type as it spawns -- either a card or deck. ---@param cardList table A list of Player Card data structures (data/metadata) @@ -2172,7 +2527,7 @@ Spawner = { } ---@param sort boolean True if this list of cards should be sorted before spawning ---@param callback? function Callback to be called after the card/deck spawns. Spawner.spawnCards = function(cardList, pos, rot, sort, callback) - if (sort) then + if sort then table.sort(cardList, Spawner.cardComparator) end @@ -2181,16 +2536,19 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback) local investigatorCards = { } for _, card in ipairs(cardList) do - if (card.metadata.type == "Investigator") then + if card.metadata.type == "Investigator" then table.insert(investigatorCards, card) - elseif (card.metadata.type == "Minicard") then + elseif card.metadata.type == "Minicard" then + -- set proper scale for minicards + card.data.Transform.scaleX = 0.6 + card.data.Transform.scaleZ = 0.6 table.insert(miniCards, card) else table.insert(standardCards, card) end end - -- Spawn each of the three types individually. Each Y position shift accounts for the thickness - -- of the spawned deck + + -- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck local position = { x = pos.x, y = pos.y, z = pos.z } Spawner.spawn(investigatorCards, position, rot, callback) @@ -2202,13 +2560,13 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback) end Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback) - if (sort) then + if sort then table.sort(cardList, Spawner.cardComparator) end local position = { x = startPos.x, y = startPos.y, z = startPos.z } -- Special handle the first row if we have less than a full single row, but only if there's a - -- reasonable max column count. Single-row spreads will send a large value for maxCols + -- reasonable max column count. Single-row spreads will send a large value for maxCols if maxCols < 100 and #cardList < maxCols then position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT) end @@ -2231,7 +2589,7 @@ Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callb end end --- Spawn a specific list of cards. This method is for internal use and should not be called +-- Spawn a specific list of cards. This method is for internal use and should not be called -- directly, use spawnCards instead. ---@param cardList table A list of Player Card data structures (data/metadata) ---@param pos table Position where the cards should be spawned (global) @@ -2246,25 +2604,18 @@ Spawner.spawn = function(cardList, pos, rot, callback) if cardList[1].data.SidewaysCard then rot = { rot.x, rot.y - 90, rot.z } end - spawnObjectData({ + return spawnObjectData({ data = cardList[1].data, position = pos, rotation = rot, callback_function = callback }) - return end -- For multiple cards, construct a deck and spawn that - local deck = Spawner.buildDeckDataTemplate() - - -- Decks won't inherently scale to the cards in them. The card list being spawned should be all - -- the same type/size by this point, so use the first card to set the size - deck.Transform = { - scaleX = cardList[1].data.Transform.scaleX, - scaleY = 1, - scaleZ = cardList[1].data.Transform.scaleZ - } + local deckScaleX = cardList[1].data.Transform.scaleX + local deckScaleZ = cardList[1].data.Transform.scaleZ + local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ) local sidewaysDeck = true for _, spawnCard in ipairs(cardList) do @@ -2279,7 +2630,7 @@ Spawner.spawn = function(cardList, pos, rot, callback) rot = { rot.x, rot.y - 90, rot.z } end - spawnObjectData({ + return spawnObjectData({ data = deck, position = pos, rotation = rot, @@ -2287,12 +2638,12 @@ Spawner.spawn = function(cardList, pos, rot, callback) }) end --- Inserts a card into the given deck. This does three things: +-- Inserts a card into the given deck. This does three things: -- 1. Add the card's data to ContainedObjects -- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's --- ID list. Note that the deck's ID list is "DeckIDs" even though it +-- ID list. Note that the deck's ID list is "DeckIDs" even though it -- contains a list of card Ids --- 3. Extract the card's CustomDeck table and add it to the deck. The deck's +-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's -- "CustomDeck" field is a list of all CustomDecks used by cards within the -- deck, keyed by the DeckID and referencing the custom deck table ---@param deck table TTS deck data structure to add to @@ -2332,20 +2683,22 @@ end -- creates a new table on each call without using metatables or previous -- definitions because we can't be sure that TTS doesn't modify the structure ---@return table deck Table containing the minimal TTS deck data structure -Spawner.buildDeckDataTemplate = function() +Spawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ) local deck = {} deck.Name = "Deck" - -- Card data. DeckIDs and CustomDeck entries will be built from the cards + -- Card data. DeckIDs and CustomDeck entries will be built from the cards deck.ContainedObjects = {} deck.DeckIDs = {} deck.CustomDeck = {} -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here + -- Decks won't inherently scale to the cards in them. The card list being spawned should be all + -- the same type/size by this point, so use the first card to set the size deck.Transform = { - scaleX = 1, + scaleX = deckScaleX or 1, scaleY = 1, - scaleZ = 1, + scaleZ = deckScaleZ or 1, } return deck @@ -2357,7 +2710,7 @@ end ---@return string id >= startId Spawner.findNextAvailableId = function(objectTable, startId) local id = startId - while (objectTable[id] ~= nil) do + while objectTable[id] ~= nil do id = tostring(tonumber(id) + 1) end return id @@ -2376,14 +2729,14 @@ Spawner.getpbcn = function(metadata) end end --- Comparison function used to sort the cards in a deck. Groups bonded or +-- Comparison function used to sort the cards in a deck. Groups bonded or -- permanent cards first, then sorts within theose types by name/subname. -- Normal cards will sort in standard alphabetical order, while -- permanent/bonded/customizable will be in reverse alphabetical order. -- -- Since cards spawn in the order provided by this comparator, with the first -- cards ending up at the bottom of a pile, this ordering will spawn in reverse --- alphabetical order. This presents the cards in order for non-face-down +-- alphabetical order. This presents the cards in order for non-face-down -- areas, and presents them in order when Searching the face-down deck. Spawner.cardComparator = function(card1, card2) local pbcn1 = Spawner.getpbcn(card1.metadata) @@ -2404,250 +2757,7 @@ Spawner.cardComparator = function(card1, card2) end end end) -__bundle_register("playercards/SpawnBag", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playercards/PlayerCardSpawner") - --- Allows spawning of defined lists of cards which will be created from the template in the All --- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while --- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the --- spawned objects. Objects moved out of this area will not be cleaned up. --- --- SpawnSpec: Spawning requires a spawn specification with the following structure: --- { --- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned, --- but each requires a separate name --- cards: A list of card IDs to be spawned --- globalPos: Where the spawned objects should be placed, in global coordinates. This should be --- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 } --- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with --- globalPos, this should be a valid Vector with x, y, and z defined --- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a --- spread moving to the right. globalPos will define the location of the first card, each --- after that will be moved a predefined distance --- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be --- laid out in before starting a new row. If spread is true but spreadCols is not set, all --- cards will be in a single row (however long that may be) --- } --- See BondedBag.ttslua for an example -do - local allCardsBagApi = require("playercards/AllCardsBagApi") - - local SpawnBag = { } - local internal = { } - - -- To assist debugging, will draw a box around the recall zone when it's set up - local SHOW_RECALL_ZONE = false - - -- Distance to expand the recall zone around any added object. - local RECALL_BUFFER_X = 0.9 - local RECALL_BUFFER_Z = 0.5 - - -- In order to mimic the behavior of the previous memory buttons we use a temporary bag when - -- recalling objects. This bag is tiny and transparent, and will be placed at the same location as - -- this object. Once all placed cards are recalled bag to this bag, it will be destroyed - local RECALL_BAG = { - Name = "Bag", - Transform = { - scaleX = 0.01, - scaleY = 0.01, - scaleZ = 0.01, - }, - ColorDiffuse = { - r = 0, - g = 0, - b = 0, - a = 0, - }, - Locked = true, - Grid = true, - Snap = false, - Tooltip = false - } - - -- Tracks what has been placed by this "bag" so they can be recalled - local placedSpecs = { } - local placedObjectGuids = { } - local recallZone = nil - - -- Loads a table of saved state, extracted during the parent object's onLoad - SpawnBag.loadFromSave = function(saveTable) - placedSpecs = saveTable.placed - placedObjectGuids = saveTable.placedObjects - recallZone = saveTable.recall - end - - -- Generates a table of save state that can be included in the parent object's onSave - SpawnBag.getStateForSave = function() - return { - placed = placedSpecs, - placedObjects = placedObjectGuids, - recall = recallZone, - } - end - - -- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples - SpawnBag.spawn = function(spawnSpec) - -- Limit to one placement at a time - if (placedSpecs[spawnSpec.name]) then - return - end - if (spawnSpec == nil) then - -- TODO: error here - return - end - local cardsToSpawn = { } - local cardList = spawnSpec.cards - for _, cardId in ipairs(cardList) do - local cardData = allCardsBagApi.getCardById(cardId) - if (cardData ~= nil) then - table.insert(cardsToSpawn, cardData) - else - -- TODO: error here - end - end - if (spawnSpec.spread) then - Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject) - else - -- TTS decks come out in reverse order of the cards, reverse the list so the input order stays - -- This only applies for decks; spreads are spawned by us in the order given - if spawnSpec.rotation.z ~= 180 then - cardsToSpawn = internal.reverseList(cardsToSpawn) - end - Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, internal.recordPlacedObject) - end - placedSpecs[spawnSpec.name] = true - end - - -- Recalls all spawned objects to the bag, and clears the placedObjectGuids list - ---@param fast boolean If true, cards will be deleted directly without faking the bag recall. - SpawnBag.recall = function(fast) - if fast then - internal.deleteSpawned() - else - internal.recallSpawned() - end - - -- We've recalled everything we can, some cards may have been moved out of the - -- card area. Just reset at this point. - placedSpecs = { } - placedObjectGuids = { } - recallZone = nil - end - - -- Deleted all spawned cards. - internal.deleteSpawned = function() - for guid, _ in pairs(placedObjectGuids) do - local obj = getObjectFromGUID(guid) - if (obj ~= nil) then - if (internal.isInRecallZone(obj)) then - obj.destruct() - end - placedObjectGuids[guid] = nil - end - end - end - - -- Recalls spawned cards with a fake bag that replicates the memory bag recall style. - internal.recallSpawned = function() - local trash = spawnObjectData({data = RECALL_BAG, position = self.getPosition()}) - for guid, _ in pairs(placedObjectGuids) do - local obj = getObjectFromGUID(guid) - if (obj ~= nil) then - if (internal.isInRecallZone(obj)) then - trash.putObject(obj) - end - placedObjectGuids[guid] = nil - end - end - - trash.destruct() - end - - - -- Callback for when an object has been spawned. Tracks the object for later recall and updates the - -- recall zone. - internal.recordPlacedObject = function(spawned) - placedObjectGuids[spawned.getGUID()] = true - internal.expandRecallZone(spawned) - end - - -- Expands the current recall zone based on the position of the given object. The recall zone will - -- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer - internal.expandRecallZone = function(spawnedCard) - local pos = spawnedCard.getPosition() - if (recallZone == nil) then - -- First card out of the bag, initialize surrounding that - recallZone = { } - recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z } - recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z } - return - else - if (pos.x > recallZone.upperLeft.x) then - recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X - end - if (pos.x < recallZone.lowerRight.x) then - recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X - end - if (pos.z > recallZone.upperLeft.z) then - recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z - end - if (pos.z < recallZone.lowerRight.z) then - recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z - end - end - if (SHOW_RECALL_ZONE) then - local y = 1.5 - local thick = 0.05 - Global.setVectorLines({ - { - points = { {recallZone.upperLeft.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.lowerRight.z} }, - color = {1,0,0}, - thickness = thick, - rotation = {0,0,0} - }, - { - points = { {recallZone.upperLeft.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.lowerRight.z} }, - color = {1,0,0}, - thickness = thick, - rotation = {0,0,0} - }, - { - points = { {recallZone.lowerRight.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.upperLeft.z} }, - color = {1,0,0}, - thickness = thick, - rotation = {0,0,0} - }, - { - points = { {recallZone.lowerRight.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.upperLeft.z} }, - color = {1,0,0}, - thickness = thick, - rotation = {0,0,0} - } - }) - end - end - - -- Checks to see if the given object is in the current recall zone. If there isn't a recall zone, - -- will return true so that everything can be easily cleaned up. - internal.isInRecallZone = function(obj) - if (recallZone == nil) then - return true - end - local pos = obj.getPosition() - return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x - and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z) - end - - internal.reverseList = function(list) - local reversed = { } - for i = 1, #list do - reversed[i] = list[#list - i + 1] - end - - return reversed - end - - return SpawnBag -end +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("playercards/PlayerCardPanel") end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Tile Player Cards 2d30ee.xml b/unpacked/Custom_Tile Player Cards 2d30ee.xml deleted file mode 100644 index 528cdd631..000000000 --- a/unpacked/Custom_Tile Player Cards 2d30ee.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - -• Select a group to place cards -• Copy the cards you want for your deck -• Select a new group to clear the placed cards and see new ones -• Clear to remove all cards - - \ No newline at end of file diff --git a/unpacked/Custom_Tile Player Cards 2d30ee.yaml b/unpacked/Custom_Tile Player Cards 2d30ee.yaml index 2f8094069..9e4ae1994 100644 --- a/unpacked/Custom_Tile Player Cards 2d30ee.yaml +++ b/unpacked/Custom_Tile Player Cards 2d30ee.yaml @@ -4,9 +4,9 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - b: 1 - g: 1 - r: 1 + b: 0 + g: 0 + r: 0 CustomImage: CustomTile: Stackable: false @@ -17,6 +17,10 @@ CustomImage: ImageSecondaryURL: '' ImageURL: http://cloud-3.steamusercontent.com/ugc/2342503777940937086/92256BDF101E6272AD1E3F5F0043D311DF708F03/ WidthScale: 0 +CustomUIAssets: +- Name: OtherCards + Type: 0 + URL: http://cloud-3.steamusercontent.com/ugc/2446096169989812196/B5C491331EB348C261F561DC7A19968ECF9FC74A/ Description: '' DragSelectable: true GMNotes: '' @@ -47,4 +51,4 @@ Transform: scaleY: 1 scaleZ: 10 Value: 0 -XmlUI: !include 'Custom_Tile Player Cards 2d30ee.xml' +XmlUI: '' diff --git a/unpacked/Custom_Tile Playermat 1 White 8b081b.ttslua b/unpacked/Custom_Tile Playermat 1 White 8b081b.ttslua index 9f92b3fcd..26a86afde 100644 --- a/unpacked/Custom_Tile Playermat 1 White 8b081b.ttslua +++ b/unpacked/Custom_Tile Playermat 1 White 8b081b.ttslua @@ -42,172 +42,7 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playermat/Playmat") -end) -__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local ChaosBagApi = {} - - -- respawns the chaos bag with a new state of tokens - ---@param tokenList table List of chaos token ids - ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getChaosBagState = function() - local chaosBagContentsCatcher = Global.call("getChaosBagState") - local chaosBagContents = {} - for _, v in ipairs(chaosBagContentsCatcher) do - table.insert(chaosBagContents, v) - end - return chaosBagContents - end - - -- checks scripting zone for chaos bag (also called by a lot of objects!) - ChaosBagApi.findChaosBag = function() - return Global.call("findChaosBag") - end - - -- returns a table of object references to the tokens in play (does not include sealed tokens!) - ChaosBagApi.getTokensInPlay = function() - return Global.call("getChaosTokensinPlay") - end - - -- returns all sealed tokens on cards to the chaos bag - ---@param playerColor string Color of the player to show the broadcast to - ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) - end - - -- returns all drawn tokens to the chaos bag - ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") - end - - -- removes the specified chaos token from the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) - end - - -- returns a chaos token to the bag and calls all relevant functions - ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) - end - - -- spawns the specified chaos token and puts it into the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) - end - - -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens - -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the - -- contents of the bag should check this method before doing so. - -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. - ChaosBagApi.canTouchChaosTokens = function() - return Global.call("canTouchChaosTokens") - end - - -- called by playermats (by the "Draw chaos token" button) - ---@param mat tts__Object Playermat that triggered this - ---@param drawAdditional boolean Controls whether additional tokens should be drawn - ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag - ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getIdUrlMap = function() - return Global.getTable("ID_URL_MAP") - end - - return ChaosBagApi -end -end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) -__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local MythosAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getMythosArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") - end - - ---@return any: Table of chaos token metadata (if provided through scenario reference card) - MythosAreaApi.returnTokenData = function() - return getMythosArea().call("returnTokenData") - end - - ---@return any: Object reference to the encounter deck - MythosAreaApi.getEncounterDeck = function() - return getMythosArea().call("getEncounterDeck") - end - - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) - end - - -- reshuffle the encounter deck - MythosAreaApi.reshuffleEncounterDeck = function() - getMythosArea().call("reshuffleEncounterDeck") - end - - return MythosAreaApi -end +require("playermat/Playermat") end) __bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules) do @@ -247,139 +82,172 @@ do return NavigationOverlayApi end end) -__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do - local OptionPanelApi = {} + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } - -- loads saved options - ---@param options table Set a new state for the option table - OptionPanelApi.loadSettings = function(options) - return Global.call("loadSettings", options) + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList end - ---@return any: Table of option panel state - OptionPanelApi.getOptions = function() - return Global.getTable("optionPanel") + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) end - return OptionPanelApi + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib end end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlayAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") + local ChaosBagApi = {} - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + -- respawns the chaos bag with a new state of tokens + ---@param tokenList table List of chaos token ids + ChaosBagApi.setChaosBagState = function(tokenList) + Global.call("setChaosBagState", tokenList) end - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getChaosBagState = function() + local chaosBagContentsCatcher = Global.call("getChaosBagState") + local chaosBagContents = {} + for _, v in ipairs(chaosBagContentsCatcher) do + table.insert(chaosBagContents, v) + end + return chaosBagContents end - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") + -- checks scripting zone for chaos bag (also called by a lot of objects!) + ChaosBagApi.findChaosBag = function() + return Global.call("findChaosBag") end - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) + -- returns a table of object references to the tokens in play (does not include sealed tokens!) + ChaosBagApi.getTokensInPlay = function() + return Global.call("getChaosTokensinPlay") end - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) + -- returns all sealed tokens on cards to the chaos bag + ---@param playerColor string Color of the player to show the broadcast to + ChaosBagApi.releaseAllSealedTokens = function(playerColor) + Global.call("releaseAllSealedTokens", playerColor) end - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) + -- returns all drawn tokens to the chaos bag + ChaosBagApi.returnChaosTokens = function() + Global.call("returnChaosTokens") end - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) + -- removes the specified chaos token from the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.removeChaosToken = function(id) + Global.call("removeChaosToken", id) end - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) + -- returns a chaos token to the bag and calls all relevant functions + ---@param token tts__Object Chaos token to return + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) end - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) + -- spawns the specified chaos token and puts it into the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.spawnChaosToken = function(id) + Global.call("spawnChaosToken", id) end - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) + -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens + -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the + -- contents of the bag should check this method before doing so. + -- This method will broadcast a message to all players if the bag is being searched. + ---@return any: True if the bag is manipulated, false if it should be blocked. + ChaosBagApi.canTouchChaosTokens = function() + return Global.call("canTouchChaosTokens") end - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) + -- draws a chaos token to a playermat + ---@param mat tts__Object Playermat that triggered this + ---@param drawAdditional boolean Controls whether additional tokens should be drawn + ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag + ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) end - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getIdUrlMap = function() + return Global.getTable("ID_URL_MAP") end - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi + return ChaosBagApi end end) __bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -420,11 +288,1612 @@ do return TokenChecker end end) +__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local OptionPanelApi = {} + + -- loads saved options + ---@param options table Set a new state for the option table + OptionPanelApi.loadSettings = function(options) + return Global.call("loadSettings", options) + end + + ---@return any: Table of option panel state + OptionPanelApi.getOptions = function() + return Global.getTable("optionPanel") + end + + return OptionPanelApi +end +end) +__bundle_register("playermat/Playermat", function(require, _LOADED, __bundle_register, __bundle_modules) +local chaosBagApi = require("chaosbag/ChaosBagApi") +local deckLib = require("util/DeckLib") +local guidReferenceApi = require("core/GUIDReferenceApi") +local mythosAreaApi = require("core/MythosAreaApi") +local navigationOverlayApi = require("core/NavigationOverlayApi") +local searchLib = require("util/SearchLib") +local tokenChecker = require("core/token/TokenChecker") +local tokenManager = require("core/token/TokenManager") +local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") + +-- we use this to turn off collision handling until onLoad() is complete +local collisionEnabled = false +local currentlyEditingSlots = false + +-- x-Values for discard buttons +local DISCARD_BUTTON_X_START = -1.365 +local DISCARD_BUTTON_X_OFFSET = 0.455 + +local SEARCH_AROUND_SELF_X_BUFFER = 8 +local SEARCH_AROUND_SELF_Z_BUFFER = 1.75 + +-- defined areas for object searching +local MAIN_PLAY_AREA = { + upperLeft = { x = 1.98, z = 0.736 }, + lowerRight = { x = -0.79, z = -0.39 } +} +local INVESTIGATOR_AREA = { + upperLeft = { x = -1.084, z = 0.06517 }, + lowerRight = { x = -1.258, z = -0.0805 } +} +local THREAT_AREA = { + upperLeft = { x = 1.53, z = -0.34 }, + lowerRight = { x = -1.13, z = -0.92 } +} +local DECK_DISCARD_AREA = { + upperLeft = { x = -1.62, z = 0.855 }, + lowerRight = { x = -2.02, z = -0.245 }, + center = { x = -1.82, y = 0.5, z = 0.305 }, + size = { x = 0.4, y = 3, z = 1.1 } +} + +-- local positions +local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } +local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +local DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 } + +-- global position of encounter discard pile +local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 } + +-- used for the buttons on the right side of the playermat +-- starts off with the data for the "Upkeep" button and will then be changed +local buttonParameters = { + label = "Upkeep", + click_function = "doUpkeep", + tooltip = "Right-click to change color", + function_owner = self, + position = { x = 1.82, y = 0.1, z = -0.45 }, + scale = { 0.12, 0.12, 0.12 }, + width = 1000, + height = 280, + font_size = 180 +} + +-- table of texture URLs +local nameToTexture = { + Guardian = "http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/", + Mystic = "http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/", + Neutral = "http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/", + Rogue = "http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/", + Seeker = "http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/", + Survivor = "http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/" +} + +-- translation table for slot names to characters for special font +local slotNameToChar = { + ["any"] = "", + ["Accessory"] = "C", + ["Ally"] = "E", + ["Arcane"] = "G", + ["Body"] = "K", + ["Hand (right)"] = "M", + ["Hand (left)"] = "M", + ["Hand x2"] = "N", + ["Tarot"] = "A" +} + +-- slot symbol for the respective slot (from top left to bottom right) - intentionally global! +slotData = {} +local defaultSlotData = { + -- 1st row + "any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally", + + -- 2nd row + "any", "any", "any", "Accessory", "Arcane", "Arcane", "Body" +} + +-- global variables for access +activeInvestigatorClass = "Neutral" +activeInvestigatorId = "00000" +hasDES = false + +local isClassTextureEnabled = true +local isDrawButtonVisible = false + +-- table of type-object reference pairs of all owned objects +local ownedObjects = {} +local matColor = self.getMemo() + +function onSave() + return JSON.encode({ + activeInvestigatorClass = activeInvestigatorClass, + activeInvestigatorId = activeInvestigatorId, + isClassTextureEnabled = isClassTextureEnabled, + isDrawButtonVisible = isDrawButtonVisible, + playerColor = playerColor, + slotData = slotData + }) +end + +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) + activeInvestigatorClass = loadedData.activeInvestigatorClass + activeInvestigatorId = loadedData.activeInvestigatorId + isClassTextureEnabled = loadedData.isClassTextureEnabled + isDrawButtonVisible = loadedData.isDrawButtonVisible + playerColor = loadedData.playerColor + slotData = loadedData.slotData + end + + updateMessageColor(playerColor) + + self.interactable = false + + -- get object references to owned objects + ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) + + -- discard button creation + for i = 1, 6 do + makeDiscardButton(i) + end + + self.createButton({ + click_function = "drawEncounterCard", + function_owner = self, + position = { -1.84, 0, -0.65 }, + rotation = { 0, 80, 0 }, + width = 265, + height = 190 + }) + + self.createButton({ + click_function = "drawChaosTokenButton", + function_owner = self, + position = { 1.85, 0, -0.74 }, + rotation = { 0, -45, 0 }, + width = 135, + height = 135 + }) + + -- Upkeep button: can use the default parameters for this + self.createButton(buttonParameters) + + -- Slot editing button: modified default data + buttonParameters.label = "Edit Slots" + buttonParameters.click_function = "toggleSlotEditing" + buttonParameters.tooltip = "Right-click to reset slot symbols" + buttonParameters.position.z = 0.92 + self.createButton(buttonParameters) + + showDrawButton(isDrawButtonVisible) + redrawSlotSymbols() + math.randomseed(os.time()) + Wait.time(function() collisionEnabled = true end, 0.1) +end + +--------------------------------------------------------- +-- utility functions +--------------------------------------------------------- + +-- searches an area and optionally filters the result +function searchArea(origin, size, filter) + return searchLib.inArea(origin, self.getRotation(), size, filter) +end + +-- finds all objects on the playermat and associated set aside zone. +function searchAroundSelf(filter) + local scale = self.getScale() + local bounds = self.getBoundsNormalized() + + -- Increase the width to cover the set aside zone + bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER + bounds.size.y = 1 + bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER + + -- 'setAsideDirection' accounts for the set aside zone being on the left or right, + -- depending on the table position of the playermat + local setAsideDirection = bounds.center.z > 0 and 1 or -1 + + -- Since the cast is centered on the position, shift left or right to keep + -- the non-set aside edge of the cast at the edge of the playermat + local localCenter = self.positionToLocal(bounds.center) + localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x + localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z + return searchArea(self.positionToWorld(localCenter), bounds.size, filter) +end + +-- searches the area around the draw deck and discard pile +function searchDeckAndDiscardArea(filter) + local pos = self.positionToWorld(DECK_DISCARD_AREA.center) + local scale = self.getScale() + local size = { + x = DECK_DISCARD_AREA.size.x * scale.x, + y = DECK_DISCARD_AREA.size.y, + z = DECK_DISCARD_AREA.size.z * scale.z + } + return searchArea(pos, size, filter) +end + +-- rounds a number to the specified amount of decimal places +---@param num number Initial value +---@param numDecimalPlaces number Amount of decimal places +---@return number: rounded number +function round(num, numDecimalPlaces) + local mult = 10 ^ (numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +-- edits the label of a button +---@param oldLabel string Old label of the button +---@param newLabel string New label of the button +function editButtonLabel(oldLabel, newLabel) + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == oldLabel then + self.editButton({ index = buttons[i].index, label = newLabel }) + end + end +end + +-- updates the internal "messageColor" which is used for print/broadcast statements if no player is seated +---@param clickedByColor string Colorstring of player who clicked a button +function updateMessageColor(clickedByColor) + messageColor = Player[playerColor].seated and playerColor or clickedByColor +end + +--------------------------------------------------------- +-- Discard buttons +--------------------------------------------------------- + +-- 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 obj.type == "Card" or obj.type == "Deck" then + if obj.hasTag("PlayerCard") then + deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) + else + deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 }) + end + elseif tokenChecker.isChaosToken(obj) then + -- put chaos tokens back into bag (e.g. Unrelenting) + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif not obj.getLock() and not obj.hasTag("DontDiscard") then + -- don't touch locked objects (like the table etc.) or specific objects (like key tokens) + ownedObjects.Trash.putObject(obj) + end + end +end + +-- build a discard button to discard from searchPosition +---@param id number Index of the discard button (from left to right, must be unique) +function makeDiscardButton(id) + local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET + local position = { xValue, 0.1, -0.94 } + local searchPosition = { -position[1], position[2], position[3] + 0.32 } + local handlerName = 'handler' .. id + 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, + function_owner = self, + position = position, + scale = { 0.12, 0.12, 0.12 }, + width = 900, + height = 350, + font_size = 220 + }) +end + +--------------------------------------------------------- +-- Upkeep button +--------------------------------------------------------- + +-- calls the Upkeep function with correct parameter +function doUpkeepFromHotkey(clickedByColor) + doUpkeep(_, clickedByColor) +end + +function doUpkeep(_, clickedByColor, isRightClick) + if isRightClick then + changeColor(clickedByColor) + return + end + + updateMessageColor(clickedByColor) + + -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum + checkForDES() + local forcedLearning = false + local rot = self.getRotation() + for _, obj in ipairs(searchAroundSelf()) do + if obj.hasTag("Temporary") == true then + discardListOfObjects({ obj }) + elseif obj.hasTag("UniversalToken") == true and obj.is_face_down then + obj.flip() + elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + if not (obj.getVar("do_not_ready") or obj.hasTag("DoNotReady")) then + local cardRotation = round(obj.getRotation().y, 0) - rot.y + local yRotDiff = 0 + + if cardRotation < 0 then + cardRotation = cardRotation + 360 + end + + -- rotate cards to the next multiple of 90° towards 0° + if cardRotation > 90 and cardRotation <= 180 then + yRotDiff = 90 + elseif cardRotation < 270 and cardRotation > 180 then + yRotDiff = 270 + end + + -- set correct rotation for face-down cards + rot.z = obj.is_face_down and 180 or 0 + obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z }) + end + + -- detect Forced Learning to handle card drawing accordingly + if cardMetadata.id == "08031" then + forcedLearning = true + end + + -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile) + if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x > -1 then + tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) + end + elseif obj.type == "Deck" and forcedLearning == false then + -- check decks for forced learning + for _, deepObj in ipairs(obj.getObjects()) do + local cardMetadata = JSON.decode(deepObj.gm_notes) or {} + if cardMetadata.id == "08031" then + forcedLearning = true + end + end + end + end + + -- flip investigator mini-card and summoned servitor mini-card + -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs) + local miniId = string.match(activeInvestigatorId, ".....") .. "-m" + for _, obj in ipairs(getObjects()) do + if obj.type == "Card" and obj.is_face_down then + local notes = JSON.decode(obj.getGMNotes()) + if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then + obj.flip() + end + end + end + + -- gain a resource (or two if playing Jenny Barnes) + if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then + updateCounter({ type = "ResourceCounter", modifier = 2 }) + printToColor("Gaining 2 resources (Jenny)", messageColor) + else + updateCounter({ type = "ResourceCounter", modifier = 1 }) + end + + -- draw a card (with handling for Patrice and Forced Learning) + if activeInvestigatorId == "06005" then + if forcedLearning then + printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?" + .. " Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) + else + -- discards all non-weakness and non-hidden cards from hand first + local handCards = Player[playerColor].getHandObjects() + local cardsToDiscard = {} + + for _, card in ipairs(handCards) do + local md = JSON.decode(card.getGMNotes()) + if card.type == "Card" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= "52020") then + table.insert(cardsToDiscard, card) + end + end + + -- perform discarding 1 by 1 + local pos = returnGlobalDiscardPosition() + deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation()) + + -- draw up to 5 cards + local cardsToDraw = 5 - #handCards + #cardsToDiscard + if cardsToDraw > 0 then + printToColor("Discarding " .. #cardsToDiscard .. " and drawing " .. cardsToDraw .. " card(s). (Patrice)", messageColor) + + -- add some time if there are any cards to discard + local k = 0 + if #cardsToDiscard > 0 then + k = 0.8 + (#cardsToDiscard * 0.1) + end + Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k) + end + end + elseif forcedLearning then + printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor) + drawCardsWithReshuffle(2) + elseif activeInvestigatorId == "89001" then + printToColor("Drawing 2 cards (Subject 5U-21)", messageColor) + drawCardsWithReshuffle(2) + else + drawCardsWithReshuffle(1) + end +end + +-- click function for "draw 1 button" (that can be added via option panel) +function doDrawOne(_, clickedByColor) + updateMessageColor(clickedByColor) + drawCardsWithReshuffle(1) +end + +-- draws the specified amount of cards (and shuffles the discard if necessary) +---@param numCards number Number of cards to draw +function drawCardsWithReshuffle(numCards) + local deckAreaObjects = getDeckAreaObjects() + + -- Norman Withers handling + local harbinger = false + if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then + harbinger = true + elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then + local cards = deckAreaObjects.draw.getObjects() + if cards[#cards].name == "The Harbinger" then + harbinger = true + end + end + + if harbinger then + printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor) + return + end + + local topCardDetected = false + if deckAreaObjects.topCard ~= nil then + deckAreaObjects.topCard.deal(1, playerColor) + topCardDetected = true + numCards = numCards - 1 + if numCards == 0 then + flipTopCardFromDeck() + return + end + end + + local deckSize = 1 + if deckAreaObjects.draw == nil then + deckSize = 0 + elseif deckAreaObjects.draw.type == "Deck" then + deckSize = #deckAreaObjects.draw.getObjects() + end + + if deckSize >= numCards then + drawCards(numCards) + -- flip top card again for Norman + if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then + flipTopCardFromDeck() + end + else + drawCards(deckSize) + if deckAreaObjects.discard ~= nil then + shuffleDiscardIntoDeck() + Wait.time(function() + drawCards(numCards - deckSize) + -- flip top card again for Norman + if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then + flipTopCardFromDeck() + end + end, 1) + end + printToColor("Take 1 horror (drawing card from empty deck)", messageColor) + end +end + +-- get the draw deck and discard pile objects and returns the references +---@return table: string-indexed table with references to the found objects +function getDeckAreaObjects() + local deckAreaObjects = {} + for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do + if self.positionToLocal(object.getPosition()).z > 0.5 then + deckAreaObjects.discard = object + -- Norman Withers handling + elseif object.type == "Card" and not object.is_face_down then + deckAreaObjects.topCard = object + else + deckAreaObjects.draw = object + end + end + return deckAreaObjects +end + +-- draws the specified number of cards (reshuffling of discard pile is handled separately) +---@param numCards number Number of cards to draw +function drawCards(numCards) + local deckAreaObjects = getDeckAreaObjects() + if deckAreaObjects.draw then + deckAreaObjects.draw.deal(numCards, playerColor) + end +end + +function shuffleDiscardIntoDeck() + local deckAreaObjects = getDeckAreaObjects() + if not deckAreaObjects.discard.is_face_down then + deckAreaObjects.discard.flip() + end + deckAreaObjects.discard.shuffle() + deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false) +end + +-- utility function for Norman Withers to flip the top card to the revealed side +function flipTopCardFromDeck() + Wait.time(function() + local deckAreaObjects = getDeckAreaObjects() + if deckAreaObjects.topCard then + elseif deckAreaObjects.draw then + if deckAreaObjects.draw.type == "Card" then + deckAreaObjects.draw.flip() + else + -- get bounds to know the height of the deck + local bounds = deckAreaObjects.draw.getBounds() + local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0) + deckAreaObjects.draw.takeObject({ position = pos, flip = true }) + end + end + end, 0.1) +end + +-- discard a random non-hidden card from hand +function doDiscardOne() + local hand = Player[playerColor].getHandObjects() + if #hand == 0 then + broadcastToColor("Cannot discard from empty hand!", messageColor, "Red") + else + local choices = {} + local hiddenCards = {} + local missingMetadataCards = {} + for i, handObj in ipairs(hand) do + if handObj.type == "Card" then + -- get a name for the card or use the index if unnamed + local name = handObj.getName() + if name == "" then + name = "Card " .. i + end + + -- check card for metadata + local md = JSON.decode(handObj.getGMNotes()) + if md == nil then + table.insert(missingMetadataCards, name) + elseif md.hidden or md.id == "52020" then + table.insert(hiddenCards, name) + else + table.insert(choices, i) + end + end + end + + -- print message with hidden cards + if #hiddenCards > 0 then + local cardList = concatenateListOfStrings(hiddenCards) + printToColor("Excluded (hidden): " .. cardList, messageColor) + end + + -- print message with missing metadata cards + if #missingMetadataCards > 0 then + local cardList = concatenateListOfStrings(missingMetadataCards) + printToColor("Excluded (missing data): " .. cardList, messageColor) + end + + if #choices == 0 then + broadcastToColor("Didn't find any eligible cards for random discarding.", messageColor, "Orange") + return + end + + -- get a random eligible card (from the "choices" table) + local num = math.random(1, #choices) + deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) + broadcastToAll(getColoredName(playerColor) .. " randomly discarded card " + .. choices[num] .. "/" .. #hand .. ".", "White") + end +end + +function concatenateListOfStrings(list) + local cardList + for _, cardName in ipairs(list) do + if not cardList then + cardList = "" + else + cardList = cardList .. ", " + end + cardList = cardList .. cardName + end + return cardList +end + +-- checks if DES is present +function checkForDES() + hasDES = false + for _, obj in ipairs(searchAroundSelf()) do + if obj.type == "Card" then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + + -- position is used to exclude deck / discard + local cardPos = self.positionToLocal(obj.getPosition()) + if cardMetadata.id == "06159" and cardPos.x > -1 then + hasDES = true + break + end + end + end +end + +--------------------------------------------------------- +-- slot symbol displaying +--------------------------------------------------------- + +-- this will redraw the XML for the slot symbols based on the slotData table +function redrawSlotSymbols() + local xml = {} + local snapId = 0 + + -- use the snap point positions in the main play area for positions + for _, snap in ipairs(self.getSnapPoints()) do + if inArea(snap.position, MAIN_PLAY_AREA) then + snapId = snapId + 1 + local slotName = slotData[snapId] + + -- conversion from regular coordinates to XML + local x = snap.position.x * 100 + local y = snap.position.z * 100 + + -- XML for a single slot (panel with text in the special font) + local slotXML = { + tag = "Panel", + attributes = { + id = "slotPanel" .. snapId, + scale = "0.1 0.1 1", + width = "175", + height = "175", + position = x .. " " .. y .. " -11" + }, + children = { + { + tag = "Text", + attributes = { + id = "slot" .. snapId, + rotation = getSlotRotation(slotName), + fontSize = "145", + font = "font_arkhamicons", + color = "#414141CB", + text = slotNameToChar[slotName] + } + } + } + } + table.insert(xml, slotXML) + end + end + + self.UI.setXmlTable(xml) +end + +-- toggle the "slot editing mode" +function toggleSlotEditing(_, clickedByColor, isRightClick) + if isRightClick then + resetSlotSymbols() + return + end + + updateMessageColor(clickedByColor) + + -- toggle internal variable + currentlyEditingSlots = not currentlyEditingSlots + + if currentlyEditingSlots then + editButtonLabel("Edit Slots", "Stop editing") + broadcastToColor("Click on a slot symbol (or an empty slot) to edit it.", messageColor, "Orange") + addClickFunctionToSlots() + else + editButtonLabel("Stop editing", "Edit Slots") + redrawSlotSymbols() + end +end + +-- click function for slot symbols during the "slot editing mode" +function slotClickfunction(player, _, id) + local slotIndex = id:gsub("slotPanel", "") + slotIndex = tonumber(slotIndex) + + -- make a list of the table keys as options for the dialog box + local slotNames = {} + for slotName, _ in pairs(slotNameToChar) do + table.insert(slotNames, slotName) + end + + -- prompt player to choose symbol + player.showOptionsDialog("Choose Slot Symbol", slotNames, slotData[slotIndex], + function(chosenSlotName) + slotData[slotIndex] = chosenSlotName + + -- update slot symbol + self.UI.setAttribute("slot" .. slotIndex, "text", slotNameToChar[chosenSlotName]) + + -- update slot rotation + self.UI.setAttribute("slot" .. slotIndex, "rotation", getSlotRotation(chosenSlotName)) + end + ) +end + +-- helper function to rotate the left hand +function getSlotRotation(slotName) + if slotName == "Hand (left)" then + return "0 180 180" + else + return "0 0 180" + end +end + +-- reset the slot symbols by making a deep copy of the default data and redrawing +function resetSlotSymbols() + slotData = {} + for _, slotName in ipairs(defaultSlotData) do + table.insert(slotData, slotName) + end + + redrawSlotSymbols() + + -- need to re-add the click functions if currently in edit mode + if currentlyEditingSlots then + addClickFunctionToSlots() + end +end + +-- enables the click functions for editing +function addClickFunctionToSlots() + for i = 1, #slotData do + self.UI.setAttribute("slotPanel" .. i, "onClick", "slotClickfunction") + end +end + +--------------------------------------------------------- +-- color related functions +--------------------------------------------------------- + +-- changes the player color +function changeColor(clickedByColor) + local colorList = Player.getColors() + + -- remove existing colors from the list of choices + for _, existingColor in ipairs(Player.getAvailableColors()) do + for i, newColor in ipairs(colorList) do + if existingColor == newColor or newColor == "Black" or newColor == "Grey" then + table.remove(colorList, i) + end + end + end + + -- show the option dialog for color selection to the player that triggered this + Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color) + -- update the color of the hand zone + local handZone = ownedObjects.HandZone + handZone.setValue(color) + + -- if the seated player clicked this, reseat him to the new color + if clickedByColor == playerColor then + navigationOverlayApi.copyVisibility(playerColor, color) + Player[playerColor].changeColor(color) + end + + -- update the internal variable + playerColor = color + end) +end + +--------------------------------------------------------- +-- playermat token spawning +--------------------------------------------------------- + +-- Finds all customizable cards in this play area and updates their metadata based on the selections +-- on the matching upgrade sheet. +-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be +-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the +-- number of customizable cards in play. +function syncAllCustomizableCards() + for _, card in ipairs(searchAroundSelf("isCard")) do + syncCustomizableMetadata(card) + end +end + +function syncCustomizableMetadata(card) + local cardMetadata = JSON.decode(card.getGMNotes()) or {} + if cardMetadata == nil or cardMetadata.customizations == nil then return end + + for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do + local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {} + if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then + for i, customization in ipairs(cardMetadata.customizations) do + if customization.replaces ~= nil and customization.replaces.uses ~= nil then + if upgradeSheet.call("isUpgradeActive", i) then + cardMetadata.uses = customization.replaces.uses + card.setGMNotes(JSON.encode(cardMetadata)) + else + -- TODO: Get the original metadata to restore it... maybe. This should only be + -- necessary in the very unlikely case that a user un-checks a previously-full upgrade + -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is + -- in place, so defer until it is + end + end + end + end + end +end + +function spawnTokensFor(object) + local extraUses = {} + if activeInvestigatorId == "03004" then + extraUses["Charge"] = 1 + end + + tokenManager.spawnForCard(object, extraUses) +end + +function onCollisionEnter(collisionInfo) + local object = collisionInfo.collision_object + + -- only continue if loading is completed + if not collisionEnabled then return end + + -- only continue for cards + if object.type ~= "Card" then return end + + maybeUpdateActiveInvestigator(object) + syncCustomizableMetadata(object) + + local localCardPos = self.positionToLocal(object.getPosition()) + if inArea(localCardPos, DECK_DISCARD_AREA) then + tokenSpawnTrackerApi.resetTokensSpawned(object) + removeTokensFromObject(object) + elseif shouldSpawnTokens(object) then + spawnTokensFor(object) + end +end + +-- checks if tokens should be spawned for the provided card +function shouldSpawnTokens(card) + if card.is_face_down then + return false + end + + local localCardPos = self.positionToLocal(card.getPosition()) + local metadata = JSON.decode(card.getGMNotes()) + + -- If no metadata we don't know the type, so only spawn in the main area + if metadata == nil then + return inArea(localCardPos, MAIN_PLAY_AREA) + end + + -- Spawn tokens for assets and events on the main area + if inArea(localCardPos, MAIN_PLAY_AREA) + and (metadata.type == "Asset" + or metadata.type == "Event") then + return true + end + + -- Spawn tokens for all encounter types in the threat area + if inArea(localCardPos, THREAT_AREA) + and (metadata.type == "Treachery" + or metadata.type == "Enemy" + or metadata.weakness) then + return true + end + + return false +end + +function onObjectEnterContainer(container, object) + if object.type ~= "Card" then return end + + local localCardPos = self.positionToLocal(object.getPosition()) + if inArea(localCardPos, DECK_DISCARD_AREA) then + tokenSpawnTrackerApi.resetTokensSpawned(object) + removeTokensFromObject(object) + end +end + +-- removes tokens from the provided card/deck +function removeTokensFromObject(object) + if object.hasTag("CardThatSeals") then + local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content) + if func ~= nil then + object.call("resetSealedTokens") + end + end + + for _, obj in ipairs(searchLib.onObject(object)) do + if tokenChecker.isChaosToken(obj) then + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif obj.getGUID() ~= "4ee1f2" and -- table + obj ~= self and + obj.type ~= "Deck" and + obj.type ~= "Card" and + obj.memo ~= nil and + obj.getLock() == false then + ownedObjects.Trash.putObject(obj) + end + end +end + +--------------------------------------------------------- +-- investigator ID grabbing and skill tracker +--------------------------------------------------------- + +-- updates the internal investigator id and action tokens if an investigator card is detected +---@param card tts__Object Card that might be an investigator +function maybeUpdateActiveInvestigator(card) + if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end + + local notes = JSON.decode(card.getGMNotes()) + local extraToken + + if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then + if notes.id == activeInvestigatorId then return end + activeInvestigatorClass = notes.class + activeInvestigatorId = notes.id + extraToken = notes.extraToken + ownedObjects.InvestigatorSkillTracker.call("updateStats", { + notes.willpowerIcons, + notes.intellectIcons, + notes.combatIcons, + notes.agilityIcons + }) + updateTexture() + elseif activeInvestigatorId ~= "00000" then + activeInvestigatorClass = "Neutral" + activeInvestigatorId = "00000" + ownedObjects.InvestigatorSkillTracker.call("updateStats", { 1, 1, 1, 1 }) + updateTexture() + else + return + end + + -- set proper scale for investigators + local cardData = card.getData() + if cardData["SidewaysCard"] == true then + -- 115% for easier readability + card.setScale({ 1.15, 1, 1.15 }) + else + -- Zoop-exported investigators are horizontal cards and TTS scales them differently + card.setScale({ 0.8214, 1, 0.8214 }) + end + + -- remove old action tokens + for _, obj in ipairs(searchAroundSelf("isUniversalToken")) do + obj.destruct() + end + + -- spawn three regular action tokens (investigator specific one in the bottom spot) + for i = 1, 3 do + local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(pos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = activeInvestigatorClass }) + end) + end + + -- spawn additional token (maybe specific for investigator) + if extraToken and extraToken ~= "None" then + -- local positions + local tokenSpawnPos = { + action = { + Vector(-0.86, 0, -0.28), -- left of the regular three actions + Vector(-1.54, 0, -0.28), -- right of the regular three actions + }, + ability = { + Vector(-1, 0, 0.118), -- bottom left corner of the investigator card + Vector(-1, 0, -0.118), -- top left corner of the investigator card + } + } + + -- spawn tokens (split string by "|") + local count = { action = 0, ability = 0 } + for str in string.gmatch(extraToken, "([^|]+)") do + local type = "action" + if str == "FreeTrigger" or str == "Reaction" then + type = "ability" + end + + count[type] = count[type] + 1 + if count[type] > 2 then + printToColor("More than two extra tokens of the same type are not supported.", playerColor) + else + local localSpawnPos = tokenSpawnPos[type][count[type]] + local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = str }) + end) + end + end + end +end + +-- updates the texture of the playermat +---@param overrideName? string Force a specific texture +function updateTexture(overrideName) + local name = "Neutral" + + -- use class specific texture if enabled + if isClassTextureEnabled then + name = activeInvestigatorClass + end + + -- get new texture URL + local newUrl = nameToTexture[name] + + -- override name if valid + if nameToTexture[overrideName] then + newUrl = nameToTexture[overrideName] + end + + -- apply texture + local customInfo = self.getCustomObject() + if customInfo.image ~= newUrl then + -- temporarily lock objects so they don't fall through the mat + local objectsToUnlock = {} + for _, obj in ipairs(searchAroundSelf()) do + if not obj.getLock() then + obj.setLock(true) + table.insert(objectsToUnlock, obj) + end + end + + self.script_state = onSave() + customInfo.image = newUrl + self.setCustomObject(customInfo) + local reloadedMat = self.reload() + + -- unlock objects when mat is reloaded + Wait.condition(function() + for _, obj in ipairs(objectsToUnlock) do + obj.setLock(false) + end + end, function() return reloadedMat.loading_custom == false end) + end +end + +--------------------------------------------------------- +-- manipulation of owned objects +--------------------------------------------------------- + +-- updates the specified owned counter +---@param param table Contains the information to update: +--- type: String Counter to target +--- newValue: Number Value to set the counter to +--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier +function updateCounter(param) + local counter = ownedObjects[param.type] + if counter ~= nil then + counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier)) + else + printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow") + end +end + +-- get the value the specified owned counter +---@param type string Counter to target +---@return number: Counter value +function getCounterValue(type) + return ownedObjects[type].getVar("val") +end + +-- set investigator skill tracker to "1, 1, 1, 1" +function resetSkillTracker() + local obj = ownedObjects.InvestigatorSkillTracker + if obj ~= nil then + obj.call("updateStats", { 1, 1, 1, 1 }) + else + printToAll("Skill tracker for " .. matColor .. " playermat could not be found.", "Yellow") + end +end + +--------------------------------------------------------- +-- calls to 'Global' / functions for calls from outside +--------------------------------------------------------- + +function drawChaosTokenButton(_, _, isRightClick) + chaosBagApi.drawChaosToken(self, isRightClick) +end + +function drawEncounterCard(_, _, isRightClick) + local drawPos = getEncounterCardDrawPosition(not isRightClick) + mythosAreaApi.drawEncounterCard(matColor, drawPos) +end + +function returnGlobalDiscardPosition() + return self.positionToWorld(DISCARD_PILE_POSITION) +end + +function returnGlobalDrawPosition() + return self.positionToWorld(DRAW_DECK_POSITION) +end + +-- returns the position for encounter card drawing +---@param stack boolean If true, returns the leftmost position instead of the first empty from the right +function getEncounterCardDrawPosition(stack) + local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION) + + -- maybe override position with first empty slot in threat area (right to left) + if not stack then + local searchPos = Vector(-0.91, 0.5, -0.625) + for i = 1, 5 do + local globalSearchPos = self.positionToWorld(searchPos) + local searchResult = searchLib.atPosition(globalSearchPos, "isCardOrDeck") + if #searchResult == 0 then + drawPos = globalSearchPos + break + else + searchPos.x = searchPos.x + 0.455 + end + end + end + + return drawPos +end + +-- creates / removes the draw 1 button +---@param visible boolean Whether the draw 1 button should be visible +function showDrawButton(visible) + isDrawButtonVisible = visible + + if isDrawButtonVisible then + -- Draw 1 button: modified default data + buttonParameters.label = "Draw 1" + buttonParameters.click_function = "doDrawOne" + buttonParameters.tooltip = "" + buttonParameters.position.z = -0.35 + self.createButton(buttonParameters) + else + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == "Draw 1" then + self.removeButton(buttons[i].index) + end + end + end +end + +-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues +---@param showCounter boolean Whether the clickable clue counter should be visible +function clickableClues(showCounter) + local clickerPos = ownedObjects.ClickableClueCounter.getPosition() + local clueCount = 0 + + -- move clue counters + local modY = showCounter and 0.525 or -0.525 + ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) + + if showCounter then + -- get current clue count + clueCount = ownedObjects.ClueCounter.getVar("exposedValue") + + -- remove clues + ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) + + -- set value for clue clickers + ownedObjects.ClickableClueCounter.call("updateVal", clueCount) + else + -- get current clue count + clueCount = ownedObjects.ClickableClueCounter.getVar("val") + + -- spawn clues + local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 }) + for i = 1, clueCount do + pos.y = pos.y + 0.045 * i + tokenManager.spawnToken(pos, "clue", self.getRotation()) + end + end +end + +-- Toggles the use of class textures +---@param state boolean Whether the class texture should be used or not +function useClassTexture(state) + if state == isClassTextureEnabled then return end + isClassTextureEnabled = state + updateTexture() +end + +-- removes all clues (moving tokens to the trash and setting counters to 0) +function removeClues() + ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) + ownedObjects.ClickableClueCounter.call("updateVal", 0) +end + +-- reports the clue count +---@param useClickableCounters boolean Controls which type of counter is getting checked +function getClueCount(useClickableCounters) + if useClickableCounters then + return ownedObjects.ClickableClueCounter.getVar("val") + else + return ownedObjects.ClueCounter.getVar("exposedValue") + end +end + +-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes +-- is true, the main card slot snap points will only snap assets, while the investigator area point +-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards. +---@param matchTypes boolean Whether snap points should only snap for the matching card types. +function setLimitSnapsByType(matchTypes) + local snaps = self.getSnapPoints() + for i, snap in ipairs(snaps) do + if inArea(snap.position, MAIN_PLAY_AREA) then + local snapTags = snaps[i].tags + if matchTypes then + if snapTags == nil then + snaps[i].tags = { "Asset" } + else + table.insert(snaps[i].tags, "Asset") + end + else + snaps[i].tags = nil + end + end + if inArea(snap.position, INVESTIGATOR_AREA) then + local snapTags = snaps[i].tags + if matchTypes then + if snapTags == nil then + snaps[i].tags = { "Investigator" } + else + table.insert(snaps[i].tags, "Investigator") + end + else + snaps[i].tags = nil + end + end + end + self.setSnapPoints(snaps) +end + +-- Simple method to check if the given point is in a specified area. Local use only +---@param point tts__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 bounds definition. +---@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) +end + +-- called by custom data helpers to add player card data +---@param args table Contains only one entry, the GUID of the custom data helper +function updatePlayerCards(args) + local customDataHelper = getObjectFromGUID(args[1]) + local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") + tokenManager.addPlayerCardData(playerCardData) +end + +-- returns the colored steam name or color +function getColoredName(playerColor) + local displayName = playerColor + if Player[playerColor].steam_name then + displayName = Player[playerColor].steam_name + end + + -- add bb-code + return "[" .. Color.fromString(playerColor):toHex() .. "]" .. displayName .. "[-]" +end +end) +__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local MythosAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getMythosArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") + end + + ---@return any: Table of chaos token metadata (if provided through scenario reference card) + MythosAreaApi.returnTokenData = function() + return getMythosArea().call("returnTokenData") + end + + ---@return any: Object reference to the encounter deck + MythosAreaApi.getEncounterDeck = function() + return getMythosArea().call("getEncounterDeck") + end + + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) + end + + -- reshuffle the encounter deck + MythosAreaApi.reshuffleEncounterDeck = function() + getMythosArea().call("reshuffleEncounterDeck") + end + + return MythosAreaApi +end +end) +__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local DeckLib = {} + local searchLib = require("util/SearchLib") + + -- places a card/deck at a position or merges into an existing deck below + ---@param objOrTable tts__Object|table Object or table of objects to move + ---@param pos table New position for the object + ---@param rot? table New rotation for the object + ---@param below? boolean Should the object be placed below an existing deck? + DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) + if objOrTable == nil or pos == nil then return end + + -- handle 'objOrTable' parameter + local objects = {} + if type(objOrTable) == "table" then + objects = objOrTable + else + table.insert(objects, objOrTable) + end + + -- search the new position for existing card/deck + local searchResult = searchLib.atPosition(pos, "isCardOrDeck") + local targetObj + + -- get new position + local offset = 0.5 + local newPos = Vector(pos) + Vector(0, offset, 0) + + if #searchResult == 1 then + targetObj = searchResult[1] + local bounds = targetObj.getBounds() + if below then + newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) + else + newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) + end + end + + -- process objects in reverse order + for i = #objects, 1, -1 do + local obj = objects[i] + -- add a 0.1 delay for each object (for animation purposes) + Wait.time(function() + -- allow moving smoothly out of hand and temporarily lock it + obj.setLock(true) + obj.use_hands = false + + if rot then + obj.setRotationSmooth(rot, false, true) + end + obj.setPositionSmooth(newPos, false, true) + + -- wait for object to finish movement (or 2 seconds) + Wait.condition( + function() + -- revert toggles + obj.setLock(false) + obj.use_hands = true + + -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting + if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then + targetObj = targetObj.putObject(obj) + else + targetObj = obj + end + end, + -- check state of the object (make sure it's not moving) + function() return obj.isDestroyed() or not obj.isSmoothMoving() end, + 2) + end, (#objects- i) * 0.1) + end + end + + return DeckLib +end +end) +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + end + + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + end + + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") + end + + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) + end + + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) + end + + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) + end + + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) + end + + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) __bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules) do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -555,13 +2024,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -576,11 +2045,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -595,18 +2064,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -618,11 +2090,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -632,7 +2103,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -658,16 +2133,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -677,9 +2152,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -715,21 +2189,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -768,7 +2234,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -778,11 +2244,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -801,7 +2267,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -818,7 +2284,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -829,7 +2295,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -902,21 +2368,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -965,8 +2426,8 @@ do return getSpawnTracker().call("markTokensSpawned", cardGuid) end - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) end TokenSpawnTracker.resetAllAssetAndEvents = function() @@ -984,1060 +2445,362 @@ do return TokenSpawnTracker end end) -__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules) -local chaosBagApi = require("chaosbag/ChaosBagApi") -local deckLib = require("util/DeckLib") -local guidReferenceApi = require("core/GUIDReferenceApi") -local mythosAreaApi = require("core/MythosAreaApi") -local navigationOverlayApi = require("core/NavigationOverlayApi") -local searchLib = require("util/SearchLib") -local tokenChecker = require("core/token/TokenChecker") -local tokenManager = require("core/token/TokenManager") - --- we use this to turn off collision handling until onLoad() is complete -local collisionEnabled = false - --- x-Values for discard buttons -local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91} - -local SEARCH_AROUND_SELF_X_BUFFER = 8 - --- defined areas for object searching -local MAIN_PLAY_AREA = { - upperLeft = { - x = 1.98, - z = 0.736 - }, - lowerRight = { - x = -0.79, - z = -0.39 - } -} -local INVESTIGATOR_AREA = { - upperLeft = { - x = -1.084, - z = 0.06517 - }, - lowerRight = { - x = -1.258, - z = -0.0805 - } -} -local THREAT_AREA = { - upperLeft = { - x = 1.53, - z = -0.34 - }, - lowerRight = { - x = -1.13, - z = -0.92 - } -} -local DECK_DISCARD_AREA = { - upperLeft = { - x = -1.62, - z = 0.855 - }, - lowerRight = { - x = -2.02, - z = -0.245 - }, - center = { - x = -1.82, - y = 0.5, - z = 0.305 - }, - size = { - x = 0.4, - y = 3, - z = 1.1 - } -} - --- local position of draw and discard pile -local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } -local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } - --- 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" - --- table of type-object reference pairs of all owned objects -local ownedObjects = {} -local matColor = self.getMemo() - --- variable to track the status of the "Show Draw Button" option -local isDrawButtonVisible = false - --- global variable to report "Dream-Enhancing Serum" status -isDES = false - -function onSave() - return JSON.encode({ - playerColor = playerColor, - activeInvestigatorId = activeInvestigatorId, - isDrawButtonVisible = isDrawButtonVisible - }) -end - -function onLoad(saveState) - self.interactable = false - - -- get object references to owned objects - ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) - - -- button creation - for i = 1, 6 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i) - end - - self.createButton({ - click_function = "drawEncounterCard", - function_owner = self, - position = {-1.84, 0, -0.65}, - rotation = {0, 80, 0}, - width = 265, - height = 190 - }) - - self.createButton({ - click_function = "drawChaosTokenButton", - function_owner = self, - position = {1.85, 0, -0.74}, - rotation = {0, -45, 0}, - width = 135, - height = 135 - }) - - self.createButton({ - label = "Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.84, 0.1, -0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) - - -- save state loading - local state = JSON.decode(saveState) - if state ~= nil then - playerColor = state.playerColor - activeInvestigatorId = state.activeInvestigatorId - isDrawButtonVisible = state.isDrawButtonVisible - end - - showDrawButton(isDrawButtonVisible) - math.randomseed(os.time()) - Wait.time(function() collisionEnabled = true end, 0.1) -end - ---------------------------------------------------------- --- utility functions ---------------------------------------------------------- - --- searches an area and optionally filters the result -function searchArea(origin, size, filter) - return searchLib.inArea(origin, self.getRotation(), size, filter) -end - --- finds all objects on the playmat and associated set aside zone. -function searchAroundSelf(filter) - local bounds = self.getBoundsNormalized() - -- Increase the width to cover the set aside zone - bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER - bounds.size.y = 1 - -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge - -- of the cast at the edge of the playmat - -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the - -- 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 - return searchArea(self.positionToWorld(localCenter), bounds.size, filter) -end - --- searches the area around the draw deck and discard pile -function searchDeckAndDiscardArea(filter) - local pos = self.positionToWorld(DECK_DISCARD_AREA.center) - local scale = self.getScale() - local size = { - x = DECK_DISCARD_AREA.size.x * scale.x, - y = DECK_DISCARD_AREA.size.y, - z = DECK_DISCARD_AREA.size.z * scale.z - } - return searchArea(pos, size, filter) -end - -function doNotReady(card) - return card.getVar("do_not_ready") or false -end - --- rounds a number to the specified amount of decimal places ----@param num number Initial value ----@param numDecimalPlaces number Amount of decimal places -function round(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) - return math.floor(num * mult + 0.5) / mult -end - ---------------------------------------------------------- --- Discard buttons ---------------------------------------------------------- - --- 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 obj.type == "Card" or obj.type == "Deck" then - if obj.hasTag("PlayerCard") then - deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) - else - deckLib.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 - chaosBagApi.returnChaosTokenToBag(obj) - -- don't touch locked objects (like the table etc.) - elseif not obj.getLock() then - ownedObjects.Trash.putObject(obj) - end - 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 handlerName = 'handler' .. number - 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, - function_owner = self, - position = {position[1], position[2], position[3] + 0.6}, - scale = {0.12, 0.12, 0.12}, - width = 900, - height = 350, - font_size = 220 - }) -end - ---------------------------------------------------------- --- Upkeep button ---------------------------------------------------------- - --- calls the Upkeep function with correct parameter -function doUpkeepFromHotkey(color) - doUpkeep(_, color) -end - -function doUpkeep(_, clickedByColor, isRightClick) - if isRightClick then - changeColor(clickedByColor) - return - end - - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or clickedByColor - - -- unexhaust cards in play zone, flip action tokens and find forcedLearning - local forcedLearning = false - local rot = self.getRotation() - for _, obj in ipairs(searchAroundSelf()) do - if obj.getDescription() == "Action Token" and obj.is_face_down then - obj.flip() - elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then - local cardMetadata = JSON.decode(obj.getGMNotes()) or {} - if not doNotReady(obj) then - local cardRotation = round(obj.getRotation().y, 0) - rot.y - local yRotDiff = 0 - - if cardRotation < 0 then - cardRotation = cardRotation + 360 - end - - -- rotate cards to the next multiple of 90° towards 0° - if cardRotation > 90 and cardRotation <= 180 then - yRotDiff = 90 - elseif cardRotation < 270 and cardRotation > 180 then - yRotDiff = 270 - end - - -- set correct rotation for face-down cards - rot.z = obj.is_face_down and 180 or 0 - obj.setRotation({rot.x, rot.y + yRotDiff, rot.z}) - end - if cardMetadata.id == "08031" then - forcedLearning = true - end - if cardMetadata.uses ~= nil then - tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) - end - end - end - - -- flip investigator mini-card and summoned servitor mini-card - -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs) - local miniId = string.match(activeInvestigatorId, ".....") .. "-m" - for _, obj in ipairs(getObjects()) do - if obj.type == "Card" and obj.is_face_down then - local notes = JSON.decode(obj.getGMNotes()) - if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then - obj.flip() - end - end - end - - -- gain a resource (or two if playing Jenny Barnes) - if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then - updateCounter({type = "ResourceCounter", modifier = 2}) - printToColor("Gaining 2 resources (Jenny)", messageColor) - else - updateCounter({type = "ResourceCounter", modifier = 1}) - end - - -- draw a card (with handling for Patrice and Forced Learning) - if activeInvestigatorId == "06005" then - if forcedLearning then - printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) - else - local handSize = #Player[playerColor].getHandObjects() - if handSize < 5 then - local cardsToDraw = 5 - handSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - drawCardsWithReshuffle(cardsToDraw) - end - end - elseif forcedLearning then - printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor) - drawCardsWithReshuffle(2) - elseif activeInvestigatorId == "89001" then - printToColor("Drawing 2 cards (Subject 5U-21)", messageColor) - drawCardsWithReshuffle(2) - else - drawCardsWithReshuffle(1) - end -end - --- function for "draw 1 button" (that can be added via option panel) -function doDrawOne(_, color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or color - drawCardsWithReshuffle(1) -end - --- draw X cards (shuffle discards if necessary) -function drawCardsWithReshuffle(numCards) - local deckAreaObjects = getDeckAreaObjects() - - -- Norman Withers handling - local harbinger = false - if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then - harbinger = true - elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then - local cards = deckAreaObjects.draw.getObjects() - if cards[#cards].name == "The Harbinger" then - harbinger = true - end - end - - if harbinger then - printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor) - return - end - - local topCardDetected = false - if deckAreaObjects.topCard ~= nil then - deckAreaObjects.topCard.deal(1, playerColor) - topCardDetected = true - numCards = numCards - 1 - if numCards == 0 then - flipTopCardFromDeck() - return - end - end - - local deckSize = 1 - if deckAreaObjects.draw == nil then - deckSize = 0 - elseif deckAreaObjects.draw.type == "Deck" then - deckSize = #deckAreaObjects.draw.getObjects() - end - - if deckSize >= numCards then - drawCards(numCards) - -- flip top card again for Norman - if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then - flipTopCardFromDeck() - end - else - drawCards(deckSize) - if deckAreaObjects.discard ~= nil then - shuffleDiscardIntoDeck() - Wait.time(function() - drawCards(numCards - deckSize) - -- flip top card again for Norman - if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then - flipTopCardFromDeck() - end - end, 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - end -end - --- get the draw deck and discard pile objects and returns the references -function getDeckAreaObjects() - local deckAreaObjects = {} - for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do - if self.positionToLocal(object.getPosition()).z > 0.5 then - deckAreaObjects.discard = object - -- Norman Withers handling - elseif object.type == "Card" and not object.is_face_down then - deckAreaObjects.topCard = object - else - deckAreaObjects.draw = object - end - end - return deckAreaObjects -end - -function drawCards(numCards) - local deckAreaObjects = getDeckAreaObjects() - if deckAreaObjects.draw then - deckAreaObjects.draw.deal(numCards, playerColor) - end -end - -function shuffleDiscardIntoDeck() - local deckAreaObjects = getDeckAreaObjects() - if not deckAreaObjects.discard.is_face_down then - deckAreaObjects.discard.flip() - end - deckAreaObjects.discard.shuffle() - deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false) -end - --- utility function for Norman Withers to flip the top card to the revealed side -function flipTopCardFromDeck() - Wait.time(function() - local deckAreaObjects = getDeckAreaObjects() - if deckAreaObjects.topCard then - elseif deckAreaObjects.draw then - if deckAreaObjects.draw.type == "Card" then - deckAreaObjects.draw.flip() - else - -- get bounds to know the height of the deck - local bounds = deckAreaObjects.draw.getBounds() - local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0) - deckAreaObjects.draw.takeObject({ position = pos, flip = true }) - end - end - end, 0.1) -end - --- discard a random non-hidden card from hand -function doDiscardOne() - local hand = Player[playerColor].getHandObjects() - if #hand == 0 then - broadcastToAll("Cannot discard from empty hand!", "Red") - else - local choices = {} - for i = 1, #hand do - local notes = JSON.decode(hand[i].getGMNotes()) - if notes ~= nil then - if notes.hidden ~= true then - table.insert(choices, i) - end - else - table.insert(choices, i) - end - end - - if #choices == 0 then - broadcastToAll("Hidden cards can't be randomly discarded.", "Orange") - return - end - - -- get a random non-hidden card (from the "choices" table) - local num = math.random(1, #choices) - deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) - - local playerName = Player[playerColor].steam_name or playerColor - broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White") - end -end - ---------------------------------------------------------- --- color related functions ---------------------------------------------------------- - --- changes the player color -function changeColor(clickedByColor) - local colorList = { - "White", - "Brown", - "Red", - "Orange", - "Yellow", - "Green", - "Teal", - "Blue", - "Purple", - "Pink" - } - - -- remove existing colors from the list of choices - for _, existingColor in ipairs(Player.getAvailableColors()) do - for i, newColor in ipairs(colorList) do - if existingColor == newColor then - table.remove(colorList, i) - end - end - end - - -- show the option dialog for color selection to the player that triggered this - Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color) - -- update the color of the hand zone - local handZone = ownedObjects.HandZone - handZone.setValue(color) - - -- if the seated player clicked this, reseat him to the new color - if clickedByColor == playerColor then - navigationOverlayApi.copyVisibility(playerColor, color) - Player[playerColor].changeColor(color) - end - - -- update the internal variable - playerColor = color - end) -end - ---------------------------------------------------------- --- playmat token spawning ---------------------------------------------------------- - --- Finds all customizable cards in this play area and updates their metadata based on the selections --- on the matching upgrade sheet. --- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be --- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the --- number of customizable cards in play. -function syncAllCustomizableCards() - for _, card in ipairs(searchAroundSelf("isCard")) do - syncCustomizableMetadata(card) - end -end - -function syncCustomizableMetadata(card) - local cardMetadata = JSON.decode(card.getGMNotes()) or { } - if cardMetadata == nil or cardMetadata.customizations == nil then - return - end - for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do - local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { } - if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then - for i, customization in ipairs(cardMetadata.customizations) do - if customization.replaces ~= nil and customization.replaces.uses ~= nil then - -- Allowed use of call(), no APIs for individual cards - if upgradeSheet.call("isUpgradeActive", i) then - cardMetadata.uses = customization.replaces.uses - card.setGMNotes(JSON.encode(cardMetadata)) - else - -- TODO: Get the original metadata to restore it... maybe. This should only be - -- necessary in the very unlikely case that a user un-checks a previously-full upgrade - -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is - -- in place, so defer until it is - end - end - end - end - end -end - -function spawnTokensFor(object) - local extraUses = { } - if activeInvestigatorId == "03004" then - extraUses["Charge"] = 1 - end - - tokenManager.spawnForCard(object, extraUses) -end - -function onCollisionEnter(collisionInfo) - local object = collisionInfo.collision_object - - -- only continue if loading is completed - if not collisionEnabled then return end - - -- only continue for cards - if object.type ~= "Card" then return end - - -- detect if "Dream-Enhancing Serum" is placed - if object.getName() == "Dream-Enhancing Serum" then isDES = true end - - maybeUpdateActiveInvestigator(object) - syncCustomizableMetadata(object) - - local localCardPos = self.positionToLocal(object.getPosition()) - if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) - removeTokensFromObject(object) - elseif shouldSpawnTokens(object) then - spawnTokensFor(object) - end -end - --- detect if "Dream-Enhancing Serum" is removed -function onCollisionExit(collisionInfo) - if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end -end - --- checks if tokens should be spawned for the provided card -function shouldSpawnTokens(card) - if card.is_face_down then - return false - end - - local localCardPos = self.positionToLocal(card.getPosition()) - local metadata = JSON.decode(card.getGMNotes()) - - -- If no metadata we don't know the type, so only spawn in the main area - if metadata == nil then - return inArea(localCardPos, MAIN_PLAY_AREA) - end - - -- Spawn tokens for assets and events on the main area - if inArea(localCardPos, MAIN_PLAY_AREA) - and (metadata.type == "Asset" - or metadata.type == "Event") then - return true - end - - -- Spawn tokens for all encounter types in the threat area - if inArea(localCardPos, THREAT_AREA) - and (metadata.type == "Treachery" - or metadata.type == "Enemy" - or metadata.weakness) then - return true - end - - return false -end - -function onObjectEnterContainer(container, object) - if object.type ~= "Card" then return end - - local localCardPos = self.positionToLocal(object.getPosition()) - if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) - removeTokensFromObject(object) - end -end - --- removes tokens from the provided card/deck -function removeTokensFromObject(object) - if object.hasTag("CardThatSeals") then - local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content) - if func ~= nil then - object.call("resetSealedTokens") - end - end - - for _, obj in ipairs(searchLib.onObject(object)) do - if tokenChecker.isChaosToken(obj) then - chaosBagApi.returnChaosTokenToBag(obj) - elseif obj.getGUID() ~= "4ee1f2" and -- table - obj ~= self and - obj.type ~= "Deck" and - obj.type ~= "Card" and - obj.memo ~= nil and - obj.getLock() == false and - obj.getDescription() ~= "Action Token" then - ownedObjects.Trash.putObject(obj) - end - end -end - ---------------------------------------------------------- --- investigator ID grabbing and skill tracker ---------------------------------------------------------- - -function maybeUpdateActiveInvestigator(card) - if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end - - local notes = JSON.decode(card.getGMNotes()) - local class - - if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then - if notes.id == activeInvestigatorId then return end - class = notes.class - activeInvestigatorId = notes.id - ownedObjects.InvestigatorSkillTracker.call("updateStats", { - notes.willpowerIcons, - notes.intellectIcons, - notes.combatIcons, - notes.agilityIcons - }) - elseif activeInvestigatorId ~= "00000" then - class = "Neutral" - activeInvestigatorId = "00000" - ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1}) - else - return - end - - -- change state of action tokens - local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1}) - local smallToken = nil - local STATE_TABLE = { - ["Guardian"] = 1, - ["Seeker"] = 2, - ["Rogue"] = 3, - ["Mystic"] = 4, - ["Survivor"] = 5, - ["Neutral"] = 6 - } - - for _, obj in ipairs(search) do - if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then - if obj.getScale().x < 0.4 then - smallToken = obj - else - setObjectState(obj, STATE_TABLE[class]) - end - end - end - - -- update the small token with special action for certain investigators - local SPECIAL_ACTIONS = { - ["04002"] = 8, -- Ursula Downs - ["01002"] = 9, -- Daisy Walker - ["01502"] = 9, -- Daisy Walker - ["01002-pb"] = 9, -- Daisy Walker - ["06003"] = 10, -- Tony Morgan - ["04003"] = 11, -- Finn Edwards - ["08016"] = 14 -- Bob Jenkins - } - - if smallToken ~= nil then - setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class]) - end -end - -function setObjectState(obj, stateId) - if obj.getStateId() ~= stateId then obj.setState(stateId) end -end - ---------------------------------------------------------- --- manipulation of owned objects ---------------------------------------------------------- - --- updates the specific owned counter ----@param param table Contains the information to update: ---- type: String Counter to target ---- newValue: Number Value to set the counter to ---- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier -function updateCounter(param) - local counter = ownedObjects[param.type] - if counter ~= nil then - counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier)) - else - printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow") - end -end - --- returns the resource counter amount ----@param type string Counter to target -function getCounterValue(type) - return ownedObjects[type].getVar("val") -end - --- set investigator skill tracker to "1, 1, 1, 1" -function resetSkillTracker() - local obj = ownedObjects.InvestigatorSkillTracker - if obj ~= nil then - obj.call("updateStats", { 1, 1, 1, 1 }) - else - printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow") - end -end - ---------------------------------------------------------- --- calls to 'Global' / functions for calls from outside ---------------------------------------------------------- - -function drawChaosTokenButton(_, _, isRightClick) - chaosBagApi.drawChaosToken(self, isRightClick) -end - -function drawEncounterCard(_, _, isRightClick) - mythosAreaApi.drawEncounterCard(self, isRightClick) -end - -function returnGlobalDiscardPosition() - return self.positionToWorld(DISCARD_PILE_POSITION) -end - --- Sets this playermat's draw 1 button to visible ----@param visible boolean Whether the draw 1 button should be visible -function showDrawButton(visible) - isDrawButtonVisible = visible - - -- create the "Draw 1" button - if isDrawButtonVisible then - self.createButton({ - label = "Draw 1", - click_function = "doDrawOne", - function_owner = self, - position = { 1.84, 0.1, -0.36 }, - scale = { 0.12, 0.12, 0.12 }, - width = 800, - height = 280, - font_size = 180 - }) - - -- remove the "Draw 1" button - else - local buttons = self.getButtons() - for i = 1, #buttons do - if buttons[i].label == "Draw 1" then - self.removeButton(buttons[i].index) - end - end - end -end - --- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues ----@param showCounter boolean Whether the clickable clue counter should be visible -function clickableClues(showCounter) - local clickerPos = ownedObjects.ClickableClueCounter.getPosition() - local clueCount = 0 - - -- move clue counters - local modY = showCounter and 0.525 or -0.525 - ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) - - if showCounter then - -- current clue count - clueCount = ownedObjects.ClueCounter.getVar("exposedValue") - - -- remove clues - ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) - - -- set value for clue clickers - ownedObjects.ClickableClueCounter.call("updateVal", clueCount) - else - -- current clue count - clueCount = ownedObjects.ClickableClueCounter.getVar("val") - - -- spawn clues - local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7}) - for i = 1, clueCount do - pos.y = pos.y + 0.045 * i - tokenManager.spawnToken(pos, "clue", self.getRotation()) - end - end -end - --- removes all clues (moving tokens to the trash and setting counters to 0) -function removeClues() - ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) - ownedObjects.ClickableClueCounter.call("updateVal", 0) -end - --- reports the clue count ----@param useClickableCounters boolean Controls which type of counter is getting checked -function getClueCount(useClickableCounters) - if useClickableCounters then - return ownedObjects.ClickableClueCounter.getVar("val") - else - return ownedObjects.ClueCounter.getVar("exposedValue") - end -end - --- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes --- is true, the main card slot snap points will only snap assets, while the investigator area point --- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards. ----@param matchTypes boolean Whether snap points should only snap for the matching card types. -function setLimitSnapsByType(matchTypes) - local snaps = self.getSnapPoints() - for i, snap in ipairs(snaps) do - local snapPos = snap.position - if inArea(snapPos, MAIN_PLAY_AREA) then - local snapTags = snaps[i].tags - if matchTypes then - if snapTags == nil then - snaps[i].tags = { "Asset" } - else - table.insert(snaps[i].tags, "Asset") - end - else - snaps[i].tags = nil - end - end - if inArea(snapPos, INVESTIGATOR_AREA) then - local snapTags = snaps[i].tags - if matchTypes then - if snapTags == nil then - snaps[i].tags = { "Investigator" } - else - table.insert(snaps[i].tags, "Investigator") - end - else - snaps[i].tags = nil - end - end - end - self.setSnapPoints(snaps) -end - --- Simple method to check if the given point is in a specified area. Local use only, ----@param point tts__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 --- bounds definition. ----@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) -end - --- called by custom data helpers to add player card data ----@param args table Contains only one entry, the GUID of the custom data helper -function updatePlayerCards(args) - local customDataHelper = getObjectFromGUID(args[1]) - local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") - tokenManager.addPlayerCardData(playerCardData) -end -end) -__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local DeckLib = {} + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } - -- places a card/deck at a position or merges into an existing deck - ---@param obj tts__Object Object to move - ---@param pos table New position for the object - ---@param rot table New rotation for the object (optional) - DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot) - if obj == nil or pos == nil then return end - - -- search the new position for existing card/deck - local searchResult = searchLib.atPosition(pos, "isCardOrDeck") - - -- get new position - local newPos - local offset = 0.5 - if #searchResult == 1 then - local bounds = searchResult[1].getBounds() - newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") else - newPos = Vector(pos) + Vector(0, offset, 0) + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } end - - -- allow moving the objects smoothly out of the hand - obj.use_hands = false - - if rot then - obj.setRotationSmooth(rot, false, true) - end - obj.setPositionSmooth(newPos, false, true) - - -- continue if the card stops smooth moving - Wait.condition( - function() - obj.use_hands = true - -- this avoids a TTS bug that merges unrelated cards that are not resting - 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 - end, - function() return not obj.isSmoothMoving() end, 3) end - return DeckLib -end -end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } - - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) + return result + end - -- filtering the result + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) end end return objList end - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end end - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end end - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib + return PlayermatApi end end) return __bundle_require("__root") diff --git a/unpacked/Custom_Tile Playermat 1 White 8b081b.yaml b/unpacked/Custom_Tile Playermat 1 White 8b081b.yaml index c07d22af0..3469e8194 100644 --- a/unpacked/Custom_Tile Playermat 1 White 8b081b.yaml +++ b/unpacked/Custom_Tile Playermat 1 White 8b081b.yaml @@ -8,61 +8,61 @@ AttachedSnapPoints: y: 0.1 z: 0.12 Tags: - - ActionToken + - UniversalToken - Position: x: -0.86 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1 + x: -1.03 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.18 + x: -1.2 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.36 + x: -1.37 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -0.63 + x: -1.54 y: 0.1 - z: 0.55 + z: -0.28 + Tags: + - UniversalToken +- Position: + x: 1.76 + y: 0.1 + z: 0.04 Tags: - Asset - Position: - x: -0.62 + x: 1.37 y: 0.1 - z: 0.02 + z: 0.04 Tags: - Asset - Position: - x: -0.18 + x: 0.98 + y: 0.1 + z: 0.04 + Tags: + - Asset +- Position: + x: 0.6 y: 0.1 z: 0.03 Tags: - Asset -- Position: - x: -0.17 - y: 0.1 - z: 0.55 - Tags: - - Asset -- Position: - x: 0.21 - y: 0.1 - z: 0.56 - Tags: - - Asset - Position: x: 0.22 y: 0.1 @@ -70,39 +70,15 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 0.6 + x: -0.18 y: 0.1 z: 0.03 Tags: - Asset - Position: - x: 0.6 + x: -0.62 y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.56 + z: 0.02 Tags: - Asset - Position: @@ -112,9 +88,39 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 1.76 + x: 1.37 y: 0.1 - z: 0.04 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.98 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.6 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.21 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: -0.17 + y: 0.1 + z: 0.55 + Tags: + - Asset +- Position: + x: -0.63 + y: 0.1 + z: 0.55 Tags: - Asset - Position: @@ -208,7 +214,7 @@ CustomImage: Type: 3 ImageScalar: 1 ImageSecondaryURL: '' - ImageURL: http://cloud-3.steamusercontent.com/ugc/2037357630681963618/E7271737B19CE0BFAAA382BEEEF497FE3E06ECC1/ + ImageURL: http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/ WidthScale: 0 Description: '' DragSelectable: true @@ -222,7 +228,8 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Tile Playermat 1 White 8b081b.ttslua' -LuaScriptState: '{"activeInvestigatorId":"00000","isDrawButtonVisible":false,"playerColor":"White"}' +LuaScriptState: '{"activeInvestigatorClass":"Neutral","activeInvestigatorId":"00000","isClassTextureEnabled":true,"isDrawButtonVisible":false,"playerColor":"White","slotData":["any","any","any","Tarot","Hand + (left)","Hand (right)","Ally","any","any","any","Accessory","Arcane","Arcane","Body"]}' MeasureMovement: false Memo: White Name: Custom_Tile diff --git a/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.ttslua b/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.ttslua index 9f92b3fcd..ca2fe1fbb 100644 --- a/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.ttslua +++ b/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.ttslua @@ -42,93 +42,78 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playermat/Playmat") +require("playermat/Playermat") end) -__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do - local ChaosBagApi = {} + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } - -- respawns the chaos bag with a new state of tokens - ---@param tokenList table List of chaos token ids - ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getChaosBagState = function() - local chaosBagContentsCatcher = Global.call("getChaosBagState") - local chaosBagContents = {} - for _, v in ipairs(chaosBagContentsCatcher) do - table.insert(chaosBagContents, v) + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] end - return chaosBagContents + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList end - -- checks scripting zone for chaos bag (also called by a lot of objects!) - ChaosBagApi.findChaosBag = function() - return Global.call("findChaosBag") + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) end - -- returns a table of object references to the tokens in play (does not include sealed tokens!) - ChaosBagApi.getTokensInPlay = function() - return Global.call("getChaosTokensinPlay") + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) end - -- returns all sealed tokens on cards to the chaos bag - ---@param playerColor string Color of the player to show the broadcast to - ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) end - -- returns all drawn tokens to the chaos bag - ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) end - -- removes the specified chaos token from the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) - end - - -- returns a chaos token to the bag and calls all relevant functions - ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) - end - - -- spawns the specified chaos token and puts it into the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) - end - - -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens - -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the - -- contents of the bag should check this method before doing so. - -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. - ChaosBagApi.canTouchChaosTokens = function() - return Global.call("canTouchChaosTokens") - end - - -- called by playermats (by the "Draw chaos token" button) - ---@param mat tts__Object Playermat that triggered this - ---@param drawAdditional boolean Controls whether additional tokens should be drawn - ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag - ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getIdUrlMap = function() - return Global.getTable("ID_URL_MAP") - end - - return ChaosBagApi + return SearchLib end end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -139,6 +124,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -146,21 +132,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -172,43 +158,16 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) -__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local MythosAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getMythosArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") - end - - ---@return any: Table of chaos token metadata (if provided through scenario reference card) - MythosAreaApi.returnTokenData = function() - return getMythosArea().call("returnTokenData") - end - - ---@return any: Object reference to the encounter deck - MythosAreaApi.getEncounterDeck = function() - return getMythosArea().call("getEncounterDeck") - end - - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) - end - - -- reshuffle the encounter deck - MythosAreaApi.reshuffleEncounterDeck = function() - getMythosArea().call("reshuffleEncounterDeck") - end - - return MythosAreaApi -end -end) __bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local NavigationOverlayApi = {} @@ -247,24 +206,6 @@ do return NavigationOverlayApi end end) -__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local OptionPanelApi = {} - - -- loads saved options - ---@param options table Set a new state for the option table - OptionPanelApi.loadSettings = function(options) - return Global.call("loadSettings", options) - end - - ---@return any: Table of option panel state - OptionPanelApi.getOptions = function() - return Global.getTable("optionPanel") - end - - return OptionPanelApi -end -end) __bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local PlayAreaApi = {} @@ -278,13 +219,13 @@ do return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end - -- Returns the current value of the investigator counter from the playmat + -- Returns the current value of the investigator counter from the playermat ---@return number: Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getInvestigatorCounter().getVar("val") end - -- Updates the current value of the investigator counter from the playmat + -- Updates the current value of the investigator counter from the playermat ---@param count number Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) getInvestigatorCounter().call("updateVal", count) @@ -325,7 +266,7 @@ do getPlayArea().call("onScenarioChanged", scenarioName) end - -- Sets this playmat's snap points to limit snapping to locations or not. + -- Sets this playermat's snap points to limit snapping to locations or not. -- If matchTypes is false, snap points will be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) @@ -338,18 +279,18 @@ do getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) end - -- counts the VP on locations in the play area + -- Counts the VP on locations in the play area PlayAreaApi.countVP = function() return getPlayArea().call("countVP") end - -- highlights all locations in the play area without metadata + -- Highlights all locations in the play area without metadata ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightMissingData = function(state) return getPlayArea().call("highlightMissingData", state) end - - -- highlights all locations in the play area with VP + + -- Highlights all locations in the play area with VP ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightCountedVP = function(state) return getPlayArea().call("countVP", state) @@ -360,15 +301,26 @@ do return getPlayArea().call("isInPlayArea", object) end + -- Returns the current surface of the play area PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end + -- Updates the surface of the play area PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call PlayAreaApi.updateLocations = function(args) @@ -382,42 +334,1391 @@ do return PlayAreaApi end end) -__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local CHAOS_TOKEN_NAMES = { - ["Elder Sign"] = true, - ["+1"] = true, - ["0"] = true, - ["-1"] = true, - ["-2"] = true, - ["-3"] = true, - ["-4"] = true, - ["-5"] = true, - ["-6"] = true, - ["-7"] = true, - ["-8"] = true, - ["Skull"] = true, - ["Cultist"] = true, - ["Tablet"] = true, - ["Elder Thing"] = true, - ["Auto-fail"] = true, - ["Bless"] = true, - ["Curse"] = true, - ["Frost"] = true +__bundle_register("playermat/Playermat", function(require, _LOADED, __bundle_register, __bundle_modules) +local chaosBagApi = require("chaosbag/ChaosBagApi") +local deckLib = require("util/DeckLib") +local guidReferenceApi = require("core/GUIDReferenceApi") +local mythosAreaApi = require("core/MythosAreaApi") +local navigationOverlayApi = require("core/NavigationOverlayApi") +local searchLib = require("util/SearchLib") +local tokenChecker = require("core/token/TokenChecker") +local tokenManager = require("core/token/TokenManager") +local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") + +-- we use this to turn off collision handling until onLoad() is complete +local collisionEnabled = false +local currentlyEditingSlots = false + +-- x-Values for discard buttons +local DISCARD_BUTTON_X_START = -1.365 +local DISCARD_BUTTON_X_OFFSET = 0.455 + +local SEARCH_AROUND_SELF_X_BUFFER = 8 +local SEARCH_AROUND_SELF_Z_BUFFER = 1.75 + +-- defined areas for object searching +local MAIN_PLAY_AREA = { + upperLeft = { x = 1.98, z = 0.736 }, + lowerRight = { x = -0.79, z = -0.39 } +} +local INVESTIGATOR_AREA = { + upperLeft = { x = -1.084, z = 0.06517 }, + lowerRight = { x = -1.258, z = -0.0805 } +} +local THREAT_AREA = { + upperLeft = { x = 1.53, z = -0.34 }, + lowerRight = { x = -1.13, z = -0.92 } +} +local DECK_DISCARD_AREA = { + upperLeft = { x = -1.62, z = 0.855 }, + lowerRight = { x = -2.02, z = -0.245 }, + center = { x = -1.82, y = 0.5, z = 0.305 }, + size = { x = 0.4, y = 3, z = 1.1 } +} + +-- local positions +local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } +local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +local DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 } + +-- global position of encounter discard pile +local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 } + +-- used for the buttons on the right side of the playermat +-- starts off with the data for the "Upkeep" button and will then be changed +local buttonParameters = { + label = "Upkeep", + click_function = "doUpkeep", + tooltip = "Right-click to change color", + function_owner = self, + position = { x = 1.82, y = 0.1, z = -0.45 }, + scale = { 0.12, 0.12, 0.12 }, + width = 1000, + height = 280, + font_size = 180 +} + +-- table of texture URLs +local nameToTexture = { + Guardian = "http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/", + Mystic = "http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/", + Neutral = "http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/", + Rogue = "http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/", + Seeker = "http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/", + Survivor = "http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/" +} + +-- translation table for slot names to characters for special font +local slotNameToChar = { + ["any"] = "", + ["Accessory"] = "C", + ["Ally"] = "E", + ["Arcane"] = "G", + ["Body"] = "K", + ["Hand (right)"] = "M", + ["Hand (left)"] = "M", + ["Hand x2"] = "N", + ["Tarot"] = "A" +} + +-- slot symbol for the respective slot (from top left to bottom right) - intentionally global! +slotData = {} +local defaultSlotData = { + -- 1st row + "any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally", + + -- 2nd row + "any", "any", "any", "Accessory", "Arcane", "Arcane", "Body" +} + +-- global variables for access +activeInvestigatorClass = "Neutral" +activeInvestigatorId = "00000" +hasDES = false + +local isClassTextureEnabled = true +local isDrawButtonVisible = false + +-- table of type-object reference pairs of all owned objects +local ownedObjects = {} +local matColor = self.getMemo() + +function onSave() + return JSON.encode({ + activeInvestigatorClass = activeInvestigatorClass, + activeInvestigatorId = activeInvestigatorId, + isClassTextureEnabled = isClassTextureEnabled, + isDrawButtonVisible = isDrawButtonVisible, + playerColor = playerColor, + slotData = slotData + }) +end + +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) + activeInvestigatorClass = loadedData.activeInvestigatorClass + activeInvestigatorId = loadedData.activeInvestigatorId + isClassTextureEnabled = loadedData.isClassTextureEnabled + isDrawButtonVisible = loadedData.isDrawButtonVisible + playerColor = loadedData.playerColor + slotData = loadedData.slotData + end + + updateMessageColor(playerColor) + + self.interactable = false + + -- get object references to owned objects + ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) + + -- discard button creation + for i = 1, 6 do + makeDiscardButton(i) + end + + self.createButton({ + click_function = "drawEncounterCard", + function_owner = self, + position = { -1.84, 0, -0.65 }, + rotation = { 0, 80, 0 }, + width = 265, + height = 190 + }) + + self.createButton({ + click_function = "drawChaosTokenButton", + function_owner = self, + position = { 1.85, 0, -0.74 }, + rotation = { 0, -45, 0 }, + width = 135, + height = 135 + }) + + -- Upkeep button: can use the default parameters for this + self.createButton(buttonParameters) + + -- Slot editing button: modified default data + buttonParameters.label = "Edit Slots" + buttonParameters.click_function = "toggleSlotEditing" + buttonParameters.tooltip = "Right-click to reset slot symbols" + buttonParameters.position.z = 0.92 + self.createButton(buttonParameters) + + showDrawButton(isDrawButtonVisible) + redrawSlotSymbols() + math.randomseed(os.time()) + Wait.time(function() collisionEnabled = true end, 0.1) +end + +--------------------------------------------------------- +-- utility functions +--------------------------------------------------------- + +-- searches an area and optionally filters the result +function searchArea(origin, size, filter) + return searchLib.inArea(origin, self.getRotation(), size, filter) +end + +-- finds all objects on the playermat and associated set aside zone. +function searchAroundSelf(filter) + local scale = self.getScale() + local bounds = self.getBoundsNormalized() + + -- Increase the width to cover the set aside zone + bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER + bounds.size.y = 1 + bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER + + -- 'setAsideDirection' accounts for the set aside zone being on the left or right, + -- depending on the table position of the playermat + local setAsideDirection = bounds.center.z > 0 and 1 or -1 + + -- Since the cast is centered on the position, shift left or right to keep + -- the non-set aside edge of the cast at the edge of the playermat + local localCenter = self.positionToLocal(bounds.center) + localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x + localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z + return searchArea(self.positionToWorld(localCenter), bounds.size, filter) +end + +-- searches the area around the draw deck and discard pile +function searchDeckAndDiscardArea(filter) + local pos = self.positionToWorld(DECK_DISCARD_AREA.center) + local scale = self.getScale() + local size = { + x = DECK_DISCARD_AREA.size.x * scale.x, + y = DECK_DISCARD_AREA.size.y, + z = DECK_DISCARD_AREA.size.z * scale.z } + return searchArea(pos, size, filter) +end - local TokenChecker = {} +-- rounds a number to the specified amount of decimal places +---@param num number Initial value +---@param numDecimalPlaces number Amount of decimal places +---@return number: rounded number +function round(num, numDecimalPlaces) + local mult = 10 ^ (numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end - -- returns true if the passed object is a chaos token (by name) - TokenChecker.isChaosToken = function(obj) - if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then - return true - else - return false +-- edits the label of a button +---@param oldLabel string Old label of the button +---@param newLabel string New label of the button +function editButtonLabel(oldLabel, newLabel) + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == oldLabel then + self.editButton({ index = buttons[i].index, label = newLabel }) + end + end +end + +-- updates the internal "messageColor" which is used for print/broadcast statements if no player is seated +---@param clickedByColor string Colorstring of player who clicked a button +function updateMessageColor(clickedByColor) + messageColor = Player[playerColor].seated and playerColor or clickedByColor +end + +--------------------------------------------------------- +-- Discard buttons +--------------------------------------------------------- + +-- 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 obj.type == "Card" or obj.type == "Deck" then + if obj.hasTag("PlayerCard") then + deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) + else + deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 }) + end + elseif tokenChecker.isChaosToken(obj) then + -- put chaos tokens back into bag (e.g. Unrelenting) + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif not obj.getLock() and not obj.hasTag("DontDiscard") then + -- don't touch locked objects (like the table etc.) or specific objects (like key tokens) + ownedObjects.Trash.putObject(obj) + end + end +end + +-- build a discard button to discard from searchPosition +---@param id number Index of the discard button (from left to right, must be unique) +function makeDiscardButton(id) + local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET + local position = { xValue, 0.1, -0.94 } + local searchPosition = { -position[1], position[2], position[3] + 0.32 } + local handlerName = 'handler' .. id + 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, + function_owner = self, + position = position, + scale = { 0.12, 0.12, 0.12 }, + width = 900, + height = 350, + font_size = 220 + }) +end + +--------------------------------------------------------- +-- Upkeep button +--------------------------------------------------------- + +-- calls the Upkeep function with correct parameter +function doUpkeepFromHotkey(clickedByColor) + doUpkeep(_, clickedByColor) +end + +function doUpkeep(_, clickedByColor, isRightClick) + if isRightClick then + changeColor(clickedByColor) + return + end + + updateMessageColor(clickedByColor) + + -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum + checkForDES() + local forcedLearning = false + local rot = self.getRotation() + for _, obj in ipairs(searchAroundSelf()) do + if obj.hasTag("Temporary") == true then + discardListOfObjects({ obj }) + elseif obj.hasTag("UniversalToken") == true and obj.is_face_down then + obj.flip() + elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + if not (obj.getVar("do_not_ready") or obj.hasTag("DoNotReady")) then + local cardRotation = round(obj.getRotation().y, 0) - rot.y + local yRotDiff = 0 + + if cardRotation < 0 then + cardRotation = cardRotation + 360 + end + + -- rotate cards to the next multiple of 90° towards 0° + if cardRotation > 90 and cardRotation <= 180 then + yRotDiff = 90 + elseif cardRotation < 270 and cardRotation > 180 then + yRotDiff = 270 + end + + -- set correct rotation for face-down cards + rot.z = obj.is_face_down and 180 or 0 + obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z }) + end + + -- detect Forced Learning to handle card drawing accordingly + if cardMetadata.id == "08031" then + forcedLearning = true + end + + -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile) + if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x > -1 then + tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) + end + elseif obj.type == "Deck" and forcedLearning == false then + -- check decks for forced learning + for _, deepObj in ipairs(obj.getObjects()) do + local cardMetadata = JSON.decode(deepObj.gm_notes) or {} + if cardMetadata.id == "08031" then + forcedLearning = true + end + end end end - return TokenChecker + -- flip investigator mini-card and summoned servitor mini-card + -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs) + local miniId = string.match(activeInvestigatorId, ".....") .. "-m" + for _, obj in ipairs(getObjects()) do + if obj.type == "Card" and obj.is_face_down then + local notes = JSON.decode(obj.getGMNotes()) + if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then + obj.flip() + end + end + end + + -- gain a resource (or two if playing Jenny Barnes) + if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then + updateCounter({ type = "ResourceCounter", modifier = 2 }) + printToColor("Gaining 2 resources (Jenny)", messageColor) + else + updateCounter({ type = "ResourceCounter", modifier = 1 }) + end + + -- draw a card (with handling for Patrice and Forced Learning) + if activeInvestigatorId == "06005" then + if forcedLearning then + printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?" + .. " Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) + else + -- discards all non-weakness and non-hidden cards from hand first + local handCards = Player[playerColor].getHandObjects() + local cardsToDiscard = {} + + for _, card in ipairs(handCards) do + local md = JSON.decode(card.getGMNotes()) + if card.type == "Card" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= "52020") then + table.insert(cardsToDiscard, card) + end + end + + -- perform discarding 1 by 1 + local pos = returnGlobalDiscardPosition() + deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation()) + + -- draw up to 5 cards + local cardsToDraw = 5 - #handCards + #cardsToDiscard + if cardsToDraw > 0 then + printToColor("Discarding " .. #cardsToDiscard .. " and drawing " .. cardsToDraw .. " card(s). (Patrice)", messageColor) + + -- add some time if there are any cards to discard + local k = 0 + if #cardsToDiscard > 0 then + k = 0.8 + (#cardsToDiscard * 0.1) + end + Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k) + end + end + elseif forcedLearning then + printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor) + drawCardsWithReshuffle(2) + elseif activeInvestigatorId == "89001" then + printToColor("Drawing 2 cards (Subject 5U-21)", messageColor) + drawCardsWithReshuffle(2) + else + drawCardsWithReshuffle(1) + end +end + +-- click function for "draw 1 button" (that can be added via option panel) +function doDrawOne(_, clickedByColor) + updateMessageColor(clickedByColor) + drawCardsWithReshuffle(1) +end + +-- draws the specified amount of cards (and shuffles the discard if necessary) +---@param numCards number Number of cards to draw +function drawCardsWithReshuffle(numCards) + local deckAreaObjects = getDeckAreaObjects() + + -- Norman Withers handling + local harbinger = false + if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then + harbinger = true + elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then + local cards = deckAreaObjects.draw.getObjects() + if cards[#cards].name == "The Harbinger" then + harbinger = true + end + end + + if harbinger then + printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor) + return + end + + local topCardDetected = false + if deckAreaObjects.topCard ~= nil then + deckAreaObjects.topCard.deal(1, playerColor) + topCardDetected = true + numCards = numCards - 1 + if numCards == 0 then + flipTopCardFromDeck() + return + end + end + + local deckSize = 1 + if deckAreaObjects.draw == nil then + deckSize = 0 + elseif deckAreaObjects.draw.type == "Deck" then + deckSize = #deckAreaObjects.draw.getObjects() + end + + if deckSize >= numCards then + drawCards(numCards) + -- flip top card again for Norman + if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then + flipTopCardFromDeck() + end + else + drawCards(deckSize) + if deckAreaObjects.discard ~= nil then + shuffleDiscardIntoDeck() + Wait.time(function() + drawCards(numCards - deckSize) + -- flip top card again for Norman + if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then + flipTopCardFromDeck() + end + end, 1) + end + printToColor("Take 1 horror (drawing card from empty deck)", messageColor) + end +end + +-- get the draw deck and discard pile objects and returns the references +---@return table: string-indexed table with references to the found objects +function getDeckAreaObjects() + local deckAreaObjects = {} + for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do + if self.positionToLocal(object.getPosition()).z > 0.5 then + deckAreaObjects.discard = object + -- Norman Withers handling + elseif object.type == "Card" and not object.is_face_down then + deckAreaObjects.topCard = object + else + deckAreaObjects.draw = object + end + end + return deckAreaObjects +end + +-- draws the specified number of cards (reshuffling of discard pile is handled separately) +---@param numCards number Number of cards to draw +function drawCards(numCards) + local deckAreaObjects = getDeckAreaObjects() + if deckAreaObjects.draw then + deckAreaObjects.draw.deal(numCards, playerColor) + end +end + +function shuffleDiscardIntoDeck() + local deckAreaObjects = getDeckAreaObjects() + if not deckAreaObjects.discard.is_face_down then + deckAreaObjects.discard.flip() + end + deckAreaObjects.discard.shuffle() + deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false) +end + +-- utility function for Norman Withers to flip the top card to the revealed side +function flipTopCardFromDeck() + Wait.time(function() + local deckAreaObjects = getDeckAreaObjects() + if deckAreaObjects.topCard then + elseif deckAreaObjects.draw then + if deckAreaObjects.draw.type == "Card" then + deckAreaObjects.draw.flip() + else + -- get bounds to know the height of the deck + local bounds = deckAreaObjects.draw.getBounds() + local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0) + deckAreaObjects.draw.takeObject({ position = pos, flip = true }) + end + end + end, 0.1) +end + +-- discard a random non-hidden card from hand +function doDiscardOne() + local hand = Player[playerColor].getHandObjects() + if #hand == 0 then + broadcastToColor("Cannot discard from empty hand!", messageColor, "Red") + else + local choices = {} + local hiddenCards = {} + local missingMetadataCards = {} + for i, handObj in ipairs(hand) do + if handObj.type == "Card" then + -- get a name for the card or use the index if unnamed + local name = handObj.getName() + if name == "" then + name = "Card " .. i + end + + -- check card for metadata + local md = JSON.decode(handObj.getGMNotes()) + if md == nil then + table.insert(missingMetadataCards, name) + elseif md.hidden or md.id == "52020" then + table.insert(hiddenCards, name) + else + table.insert(choices, i) + end + end + end + + -- print message with hidden cards + if #hiddenCards > 0 then + local cardList = concatenateListOfStrings(hiddenCards) + printToColor("Excluded (hidden): " .. cardList, messageColor) + end + + -- print message with missing metadata cards + if #missingMetadataCards > 0 then + local cardList = concatenateListOfStrings(missingMetadataCards) + printToColor("Excluded (missing data): " .. cardList, messageColor) + end + + if #choices == 0 then + broadcastToColor("Didn't find any eligible cards for random discarding.", messageColor, "Orange") + return + end + + -- get a random eligible card (from the "choices" table) + local num = math.random(1, #choices) + deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) + broadcastToAll(getColoredName(playerColor) .. " randomly discarded card " + .. choices[num] .. "/" .. #hand .. ".", "White") + end +end + +function concatenateListOfStrings(list) + local cardList + for _, cardName in ipairs(list) do + if not cardList then + cardList = "" + else + cardList = cardList .. ", " + end + cardList = cardList .. cardName + end + return cardList +end + +-- checks if DES is present +function checkForDES() + hasDES = false + for _, obj in ipairs(searchAroundSelf()) do + if obj.type == "Card" then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + + -- position is used to exclude deck / discard + local cardPos = self.positionToLocal(obj.getPosition()) + if cardMetadata.id == "06159" and cardPos.x > -1 then + hasDES = true + break + end + end + end +end + +--------------------------------------------------------- +-- slot symbol displaying +--------------------------------------------------------- + +-- this will redraw the XML for the slot symbols based on the slotData table +function redrawSlotSymbols() + local xml = {} + local snapId = 0 + + -- use the snap point positions in the main play area for positions + for _, snap in ipairs(self.getSnapPoints()) do + if inArea(snap.position, MAIN_PLAY_AREA) then + snapId = snapId + 1 + local slotName = slotData[snapId] + + -- conversion from regular coordinates to XML + local x = snap.position.x * 100 + local y = snap.position.z * 100 + + -- XML for a single slot (panel with text in the special font) + local slotXML = { + tag = "Panel", + attributes = { + id = "slotPanel" .. snapId, + scale = "0.1 0.1 1", + width = "175", + height = "175", + position = x .. " " .. y .. " -11" + }, + children = { + { + tag = "Text", + attributes = { + id = "slot" .. snapId, + rotation = getSlotRotation(slotName), + fontSize = "145", + font = "font_arkhamicons", + color = "#414141CB", + text = slotNameToChar[slotName] + } + } + } + } + table.insert(xml, slotXML) + end + end + + self.UI.setXmlTable(xml) +end + +-- toggle the "slot editing mode" +function toggleSlotEditing(_, clickedByColor, isRightClick) + if isRightClick then + resetSlotSymbols() + return + end + + updateMessageColor(clickedByColor) + + -- toggle internal variable + currentlyEditingSlots = not currentlyEditingSlots + + if currentlyEditingSlots then + editButtonLabel("Edit Slots", "Stop editing") + broadcastToColor("Click on a slot symbol (or an empty slot) to edit it.", messageColor, "Orange") + addClickFunctionToSlots() + else + editButtonLabel("Stop editing", "Edit Slots") + redrawSlotSymbols() + end +end + +-- click function for slot symbols during the "slot editing mode" +function slotClickfunction(player, _, id) + local slotIndex = id:gsub("slotPanel", "") + slotIndex = tonumber(slotIndex) + + -- make a list of the table keys as options for the dialog box + local slotNames = {} + for slotName, _ in pairs(slotNameToChar) do + table.insert(slotNames, slotName) + end + + -- prompt player to choose symbol + player.showOptionsDialog("Choose Slot Symbol", slotNames, slotData[slotIndex], + function(chosenSlotName) + slotData[slotIndex] = chosenSlotName + + -- update slot symbol + self.UI.setAttribute("slot" .. slotIndex, "text", slotNameToChar[chosenSlotName]) + + -- update slot rotation + self.UI.setAttribute("slot" .. slotIndex, "rotation", getSlotRotation(chosenSlotName)) + end + ) +end + +-- helper function to rotate the left hand +function getSlotRotation(slotName) + if slotName == "Hand (left)" then + return "0 180 180" + else + return "0 0 180" + end +end + +-- reset the slot symbols by making a deep copy of the default data and redrawing +function resetSlotSymbols() + slotData = {} + for _, slotName in ipairs(defaultSlotData) do + table.insert(slotData, slotName) + end + + redrawSlotSymbols() + + -- need to re-add the click functions if currently in edit mode + if currentlyEditingSlots then + addClickFunctionToSlots() + end +end + +-- enables the click functions for editing +function addClickFunctionToSlots() + for i = 1, #slotData do + self.UI.setAttribute("slotPanel" .. i, "onClick", "slotClickfunction") + end +end + +--------------------------------------------------------- +-- color related functions +--------------------------------------------------------- + +-- changes the player color +function changeColor(clickedByColor) + local colorList = Player.getColors() + + -- remove existing colors from the list of choices + for _, existingColor in ipairs(Player.getAvailableColors()) do + for i, newColor in ipairs(colorList) do + if existingColor == newColor or newColor == "Black" or newColor == "Grey" then + table.remove(colorList, i) + end + end + end + + -- show the option dialog for color selection to the player that triggered this + Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color) + -- update the color of the hand zone + local handZone = ownedObjects.HandZone + handZone.setValue(color) + + -- if the seated player clicked this, reseat him to the new color + if clickedByColor == playerColor then + navigationOverlayApi.copyVisibility(playerColor, color) + Player[playerColor].changeColor(color) + end + + -- update the internal variable + playerColor = color + end) +end + +--------------------------------------------------------- +-- playermat token spawning +--------------------------------------------------------- + +-- Finds all customizable cards in this play area and updates their metadata based on the selections +-- on the matching upgrade sheet. +-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be +-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the +-- number of customizable cards in play. +function syncAllCustomizableCards() + for _, card in ipairs(searchAroundSelf("isCard")) do + syncCustomizableMetadata(card) + end +end + +function syncCustomizableMetadata(card) + local cardMetadata = JSON.decode(card.getGMNotes()) or {} + if cardMetadata == nil or cardMetadata.customizations == nil then return end + + for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do + local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {} + if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then + for i, customization in ipairs(cardMetadata.customizations) do + if customization.replaces ~= nil and customization.replaces.uses ~= nil then + if upgradeSheet.call("isUpgradeActive", i) then + cardMetadata.uses = customization.replaces.uses + card.setGMNotes(JSON.encode(cardMetadata)) + else + -- TODO: Get the original metadata to restore it... maybe. This should only be + -- necessary in the very unlikely case that a user un-checks a previously-full upgrade + -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is + -- in place, so defer until it is + end + end + end + end + end +end + +function spawnTokensFor(object) + local extraUses = {} + if activeInvestigatorId == "03004" then + extraUses["Charge"] = 1 + end + + tokenManager.spawnForCard(object, extraUses) +end + +function onCollisionEnter(collisionInfo) + local object = collisionInfo.collision_object + + -- only continue if loading is completed + if not collisionEnabled then return end + + -- only continue for cards + if object.type ~= "Card" then return end + + maybeUpdateActiveInvestigator(object) + syncCustomizableMetadata(object) + + local localCardPos = self.positionToLocal(object.getPosition()) + if inArea(localCardPos, DECK_DISCARD_AREA) then + tokenSpawnTrackerApi.resetTokensSpawned(object) + removeTokensFromObject(object) + elseif shouldSpawnTokens(object) then + spawnTokensFor(object) + end +end + +-- checks if tokens should be spawned for the provided card +function shouldSpawnTokens(card) + if card.is_face_down then + return false + end + + local localCardPos = self.positionToLocal(card.getPosition()) + local metadata = JSON.decode(card.getGMNotes()) + + -- If no metadata we don't know the type, so only spawn in the main area + if metadata == nil then + return inArea(localCardPos, MAIN_PLAY_AREA) + end + + -- Spawn tokens for assets and events on the main area + if inArea(localCardPos, MAIN_PLAY_AREA) + and (metadata.type == "Asset" + or metadata.type == "Event") then + return true + end + + -- Spawn tokens for all encounter types in the threat area + if inArea(localCardPos, THREAT_AREA) + and (metadata.type == "Treachery" + or metadata.type == "Enemy" + or metadata.weakness) then + return true + end + + return false +end + +function onObjectEnterContainer(container, object) + if object.type ~= "Card" then return end + + local localCardPos = self.positionToLocal(object.getPosition()) + if inArea(localCardPos, DECK_DISCARD_AREA) then + tokenSpawnTrackerApi.resetTokensSpawned(object) + removeTokensFromObject(object) + end +end + +-- removes tokens from the provided card/deck +function removeTokensFromObject(object) + if object.hasTag("CardThatSeals") then + local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content) + if func ~= nil then + object.call("resetSealedTokens") + end + end + + for _, obj in ipairs(searchLib.onObject(object)) do + if tokenChecker.isChaosToken(obj) then + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif obj.getGUID() ~= "4ee1f2" and -- table + obj ~= self and + obj.type ~= "Deck" and + obj.type ~= "Card" and + obj.memo ~= nil and + obj.getLock() == false then + ownedObjects.Trash.putObject(obj) + end + end +end + +--------------------------------------------------------- +-- investigator ID grabbing and skill tracker +--------------------------------------------------------- + +-- updates the internal investigator id and action tokens if an investigator card is detected +---@param card tts__Object Card that might be an investigator +function maybeUpdateActiveInvestigator(card) + if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end + + local notes = JSON.decode(card.getGMNotes()) + local extraToken + + if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then + if notes.id == activeInvestigatorId then return end + activeInvestigatorClass = notes.class + activeInvestigatorId = notes.id + extraToken = notes.extraToken + ownedObjects.InvestigatorSkillTracker.call("updateStats", { + notes.willpowerIcons, + notes.intellectIcons, + notes.combatIcons, + notes.agilityIcons + }) + updateTexture() + elseif activeInvestigatorId ~= "00000" then + activeInvestigatorClass = "Neutral" + activeInvestigatorId = "00000" + ownedObjects.InvestigatorSkillTracker.call("updateStats", { 1, 1, 1, 1 }) + updateTexture() + else + return + end + + -- set proper scale for investigators + local cardData = card.getData() + if cardData["SidewaysCard"] == true then + -- 115% for easier readability + card.setScale({ 1.15, 1, 1.15 }) + else + -- Zoop-exported investigators are horizontal cards and TTS scales them differently + card.setScale({ 0.8214, 1, 0.8214 }) + end + + -- remove old action tokens + for _, obj in ipairs(searchAroundSelf("isUniversalToken")) do + obj.destruct() + end + + -- spawn three regular action tokens (investigator specific one in the bottom spot) + for i = 1, 3 do + local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(pos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = activeInvestigatorClass }) + end) + end + + -- spawn additional token (maybe specific for investigator) + if extraToken and extraToken ~= "None" then + -- local positions + local tokenSpawnPos = { + action = { + Vector(-0.86, 0, -0.28), -- left of the regular three actions + Vector(-1.54, 0, -0.28), -- right of the regular three actions + }, + ability = { + Vector(-1, 0, 0.118), -- bottom left corner of the investigator card + Vector(-1, 0, -0.118), -- top left corner of the investigator card + } + } + + -- spawn tokens (split string by "|") + local count = { action = 0, ability = 0 } + for str in string.gmatch(extraToken, "([^|]+)") do + local type = "action" + if str == "FreeTrigger" or str == "Reaction" then + type = "ability" + end + + count[type] = count[type] + 1 + if count[type] > 2 then + printToColor("More than two extra tokens of the same type are not supported.", playerColor) + else + local localSpawnPos = tokenSpawnPos[type][count[type]] + local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = str }) + end) + end + end + end +end + +-- updates the texture of the playermat +---@param overrideName? string Force a specific texture +function updateTexture(overrideName) + local name = "Neutral" + + -- use class specific texture if enabled + if isClassTextureEnabled then + name = activeInvestigatorClass + end + + -- get new texture URL + local newUrl = nameToTexture[name] + + -- override name if valid + if nameToTexture[overrideName] then + newUrl = nameToTexture[overrideName] + end + + -- apply texture + local customInfo = self.getCustomObject() + if customInfo.image ~= newUrl then + -- temporarily lock objects so they don't fall through the mat + local objectsToUnlock = {} + for _, obj in ipairs(searchAroundSelf()) do + if not obj.getLock() then + obj.setLock(true) + table.insert(objectsToUnlock, obj) + end + end + + self.script_state = onSave() + customInfo.image = newUrl + self.setCustomObject(customInfo) + local reloadedMat = self.reload() + + -- unlock objects when mat is reloaded + Wait.condition(function() + for _, obj in ipairs(objectsToUnlock) do + obj.setLock(false) + end + end, function() return reloadedMat.loading_custom == false end) + end +end + +--------------------------------------------------------- +-- manipulation of owned objects +--------------------------------------------------------- + +-- updates the specified owned counter +---@param param table Contains the information to update: +--- type: String Counter to target +--- newValue: Number Value to set the counter to +--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier +function updateCounter(param) + local counter = ownedObjects[param.type] + if counter ~= nil then + counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier)) + else + printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow") + end +end + +-- get the value the specified owned counter +---@param type string Counter to target +---@return number: Counter value +function getCounterValue(type) + return ownedObjects[type].getVar("val") +end + +-- set investigator skill tracker to "1, 1, 1, 1" +function resetSkillTracker() + local obj = ownedObjects.InvestigatorSkillTracker + if obj ~= nil then + obj.call("updateStats", { 1, 1, 1, 1 }) + else + printToAll("Skill tracker for " .. matColor .. " playermat could not be found.", "Yellow") + end +end + +--------------------------------------------------------- +-- calls to 'Global' / functions for calls from outside +--------------------------------------------------------- + +function drawChaosTokenButton(_, _, isRightClick) + chaosBagApi.drawChaosToken(self, isRightClick) +end + +function drawEncounterCard(_, _, isRightClick) + local drawPos = getEncounterCardDrawPosition(not isRightClick) + mythosAreaApi.drawEncounterCard(matColor, drawPos) +end + +function returnGlobalDiscardPosition() + return self.positionToWorld(DISCARD_PILE_POSITION) +end + +function returnGlobalDrawPosition() + return self.positionToWorld(DRAW_DECK_POSITION) +end + +-- returns the position for encounter card drawing +---@param stack boolean If true, returns the leftmost position instead of the first empty from the right +function getEncounterCardDrawPosition(stack) + local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION) + + -- maybe override position with first empty slot in threat area (right to left) + if not stack then + local searchPos = Vector(-0.91, 0.5, -0.625) + for i = 1, 5 do + local globalSearchPos = self.positionToWorld(searchPos) + local searchResult = searchLib.atPosition(globalSearchPos, "isCardOrDeck") + if #searchResult == 0 then + drawPos = globalSearchPos + break + else + searchPos.x = searchPos.x + 0.455 + end + end + end + + return drawPos +end + +-- creates / removes the draw 1 button +---@param visible boolean Whether the draw 1 button should be visible +function showDrawButton(visible) + isDrawButtonVisible = visible + + if isDrawButtonVisible then + -- Draw 1 button: modified default data + buttonParameters.label = "Draw 1" + buttonParameters.click_function = "doDrawOne" + buttonParameters.tooltip = "" + buttonParameters.position.z = -0.35 + self.createButton(buttonParameters) + else + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == "Draw 1" then + self.removeButton(buttons[i].index) + end + end + end +end + +-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues +---@param showCounter boolean Whether the clickable clue counter should be visible +function clickableClues(showCounter) + local clickerPos = ownedObjects.ClickableClueCounter.getPosition() + local clueCount = 0 + + -- move clue counters + local modY = showCounter and 0.525 or -0.525 + ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) + + if showCounter then + -- get current clue count + clueCount = ownedObjects.ClueCounter.getVar("exposedValue") + + -- remove clues + ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) + + -- set value for clue clickers + ownedObjects.ClickableClueCounter.call("updateVal", clueCount) + else + -- get current clue count + clueCount = ownedObjects.ClickableClueCounter.getVar("val") + + -- spawn clues + local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 }) + for i = 1, clueCount do + pos.y = pos.y + 0.045 * i + tokenManager.spawnToken(pos, "clue", self.getRotation()) + end + end +end + +-- Toggles the use of class textures +---@param state boolean Whether the class texture should be used or not +function useClassTexture(state) + if state == isClassTextureEnabled then return end + isClassTextureEnabled = state + updateTexture() +end + +-- removes all clues (moving tokens to the trash and setting counters to 0) +function removeClues() + ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) + ownedObjects.ClickableClueCounter.call("updateVal", 0) +end + +-- reports the clue count +---@param useClickableCounters boolean Controls which type of counter is getting checked +function getClueCount(useClickableCounters) + if useClickableCounters then + return ownedObjects.ClickableClueCounter.getVar("val") + else + return ownedObjects.ClueCounter.getVar("exposedValue") + end +end + +-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes +-- is true, the main card slot snap points will only snap assets, while the investigator area point +-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards. +---@param matchTypes boolean Whether snap points should only snap for the matching card types. +function setLimitSnapsByType(matchTypes) + local snaps = self.getSnapPoints() + for i, snap in ipairs(snaps) do + if inArea(snap.position, MAIN_PLAY_AREA) then + local snapTags = snaps[i].tags + if matchTypes then + if snapTags == nil then + snaps[i].tags = { "Asset" } + else + table.insert(snaps[i].tags, "Asset") + end + else + snaps[i].tags = nil + end + end + if inArea(snap.position, INVESTIGATOR_AREA) then + local snapTags = snaps[i].tags + if matchTypes then + if snapTags == nil then + snaps[i].tags = { "Investigator" } + else + table.insert(snaps[i].tags, "Investigator") + end + else + snaps[i].tags = nil + end + end + end + self.setSnapPoints(snaps) +end + +-- Simple method to check if the given point is in a specified area. Local use only +---@param point tts__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 bounds definition. +---@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) +end + +-- called by custom data helpers to add player card data +---@param args table Contains only one entry, the GUID of the custom data helper +function updatePlayerCards(args) + local customDataHelper = getObjectFromGUID(args[1]) + local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") + tokenManager.addPlayerCardData(playerCardData) +end + +-- returns the colored steam name or color +function getColoredName(playerColor) + local displayName = playerColor + if Player[playerColor].steam_name then + displayName = Player[playerColor].steam_name + end + + -- add bb-code + return "[" .. Color.fromString(playerColor):toHex() .. "]" .. displayName .. "[-]" +end +end) +__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local ChaosBagApi = {} + + -- respawns the chaos bag with a new state of tokens + ---@param tokenList table List of chaos token ids + ChaosBagApi.setChaosBagState = function(tokenList) + Global.call("setChaosBagState", tokenList) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getChaosBagState = function() + local chaosBagContentsCatcher = Global.call("getChaosBagState") + local chaosBagContents = {} + for _, v in ipairs(chaosBagContentsCatcher) do + table.insert(chaosBagContents, v) + end + return chaosBagContents + end + + -- checks scripting zone for chaos bag (also called by a lot of objects!) + ChaosBagApi.findChaosBag = function() + return Global.call("findChaosBag") + end + + -- returns a table of object references to the tokens in play (does not include sealed tokens!) + ChaosBagApi.getTokensInPlay = function() + return Global.call("getChaosTokensinPlay") + end + + -- returns all sealed tokens on cards to the chaos bag + ---@param playerColor string Color of the player to show the broadcast to + ChaosBagApi.releaseAllSealedTokens = function(playerColor) + Global.call("releaseAllSealedTokens", playerColor) + end + + -- returns all drawn tokens to the chaos bag + ChaosBagApi.returnChaosTokens = function() + Global.call("returnChaosTokens") + end + + -- removes the specified chaos token from the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.removeChaosToken = function(id) + Global.call("removeChaosToken", id) + end + + -- returns a chaos token to the bag and calls all relevant functions + ---@param token tts__Object Chaos token to return + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) + end + + -- spawns the specified chaos token and puts it into the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.spawnChaosToken = function(id) + Global.call("spawnChaosToken", id) + end + + -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens + -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the + -- contents of the bag should check this method before doing so. + -- This method will broadcast a message to all players if the bag is being searched. + ---@return any: True if the bag is manipulated, false if it should be blocked. + ChaosBagApi.canTouchChaosTokens = function() + return Global.call("canTouchChaosTokens") + end + + -- draws a chaos token to a playermat + ---@param mat tts__Object Playermat that triggered this + ---@param drawAdditional boolean Controls whether additional tokens should be drawn + ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag + ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getIdUrlMap = function() + return Global.getTable("ID_URL_MAP") + end + + return ChaosBagApi end end) __bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -425,6 +1726,7 @@ do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -555,13 +1857,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -576,11 +1878,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -595,18 +1897,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -618,11 +1923,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -632,7 +1936,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -658,16 +1966,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -677,9 +1985,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -715,21 +2022,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -768,7 +2067,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -778,11 +2077,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -801,7 +2100,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -818,7 +2117,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -829,7 +2128,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -902,21 +2201,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -948,6 +2242,513 @@ do return TokenManager end end) +__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local DeckLib = {} + local searchLib = require("util/SearchLib") + + -- places a card/deck at a position or merges into an existing deck below + ---@param objOrTable tts__Object|table Object or table of objects to move + ---@param pos table New position for the object + ---@param rot? table New rotation for the object + ---@param below? boolean Should the object be placed below an existing deck? + DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) + if objOrTable == nil or pos == nil then return end + + -- handle 'objOrTable' parameter + local objects = {} + if type(objOrTable) == "table" then + objects = objOrTable + else + table.insert(objects, objOrTable) + end + + -- search the new position for existing card/deck + local searchResult = searchLib.atPosition(pos, "isCardOrDeck") + local targetObj + + -- get new position + local offset = 0.5 + local newPos = Vector(pos) + Vector(0, offset, 0) + + if #searchResult == 1 then + targetObj = searchResult[1] + local bounds = targetObj.getBounds() + if below then + newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) + else + newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) + end + end + + -- process objects in reverse order + for i = #objects, 1, -1 do + local obj = objects[i] + -- add a 0.1 delay for each object (for animation purposes) + Wait.time(function() + -- allow moving smoothly out of hand and temporarily lock it + obj.setLock(true) + obj.use_hands = false + + if rot then + obj.setRotationSmooth(rot, false, true) + end + obj.setPositionSmooth(newPos, false, true) + + -- wait for object to finish movement (or 2 seconds) + Wait.condition( + function() + -- revert toggles + obj.setLock(false) + obj.use_hands = true + + -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting + if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then + targetObj = targetObj.putObject(obj) + else + targetObj = obj + end + end, + -- check state of the object (make sure it's not moving) + function() return obj.isDestroyed() or not obj.isSmoothMoving() end, + 2) + end, (#objects- i) * 0.1) + end + end + + return DeckLib +end +end) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") + else + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } + end + end + + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end + end + return result + end + + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) + local objList = {} + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) + end + end + return objList + end + + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end + end + + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end + end + + return PlayermatApi +end +end) +__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local MythosAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getMythosArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") + end + + ---@return any: Table of chaos token metadata (if provided through scenario reference card) + MythosAreaApi.returnTokenData = function() + return getMythosArea().call("returnTokenData") + end + + ---@return any: Object reference to the encounter deck + MythosAreaApi.getEncounterDeck = function() + return getMythosArea().call("getEncounterDeck") + end + + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) + end + + -- reshuffle the encounter deck + MythosAreaApi.reshuffleEncounterDeck = function() + getMythosArea().call("reshuffleEncounterDeck") + end + + return MythosAreaApi +end +end) +__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local CHAOS_TOKEN_NAMES = { + ["Elder Sign"] = true, + ["+1"] = true, + ["0"] = true, + ["-1"] = true, + ["-2"] = true, + ["-3"] = true, + ["-4"] = true, + ["-5"] = true, + ["-6"] = true, + ["-7"] = true, + ["-8"] = true, + ["Skull"] = true, + ["Cultist"] = true, + ["Tablet"] = true, + ["Elder Thing"] = true, + ["Auto-fail"] = true, + ["Bless"] = true, + ["Curse"] = true, + ["Frost"] = true + } + + local TokenChecker = {} + + -- returns true if the passed object is a chaos token (by name) + TokenChecker.isChaosToken = function(obj) + if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then + return true + else + return false + end + end + + return TokenChecker +end +end) __bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local TokenSpawnTracker = {} @@ -965,8 +2766,8 @@ do return getSpawnTracker().call("markTokensSpawned", cardGuid) end - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) end TokenSpawnTracker.resetAllAssetAndEvents = function() @@ -984,1060 +2785,22 @@ do return TokenSpawnTracker end end) -__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules) -local chaosBagApi = require("chaosbag/ChaosBagApi") -local deckLib = require("util/DeckLib") -local guidReferenceApi = require("core/GUIDReferenceApi") -local mythosAreaApi = require("core/MythosAreaApi") -local navigationOverlayApi = require("core/NavigationOverlayApi") -local searchLib = require("util/SearchLib") -local tokenChecker = require("core/token/TokenChecker") -local tokenManager = require("core/token/TokenManager") - --- we use this to turn off collision handling until onLoad() is complete -local collisionEnabled = false - --- x-Values for discard buttons -local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91} - -local SEARCH_AROUND_SELF_X_BUFFER = 8 - --- defined areas for object searching -local MAIN_PLAY_AREA = { - upperLeft = { - x = 1.98, - z = 0.736 - }, - lowerRight = { - x = -0.79, - z = -0.39 - } -} -local INVESTIGATOR_AREA = { - upperLeft = { - x = -1.084, - z = 0.06517 - }, - lowerRight = { - x = -1.258, - z = -0.0805 - } -} -local THREAT_AREA = { - upperLeft = { - x = 1.53, - z = -0.34 - }, - lowerRight = { - x = -1.13, - z = -0.92 - } -} -local DECK_DISCARD_AREA = { - upperLeft = { - x = -1.62, - z = 0.855 - }, - lowerRight = { - x = -2.02, - z = -0.245 - }, - center = { - x = -1.82, - y = 0.5, - z = 0.305 - }, - size = { - x = 0.4, - y = 3, - z = 1.1 - } -} - --- local position of draw and discard pile -local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } -local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } - --- 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" - --- table of type-object reference pairs of all owned objects -local ownedObjects = {} -local matColor = self.getMemo() - --- variable to track the status of the "Show Draw Button" option -local isDrawButtonVisible = false - --- global variable to report "Dream-Enhancing Serum" status -isDES = false - -function onSave() - return JSON.encode({ - playerColor = playerColor, - activeInvestigatorId = activeInvestigatorId, - isDrawButtonVisible = isDrawButtonVisible - }) -end - -function onLoad(saveState) - self.interactable = false - - -- get object references to owned objects - ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) - - -- button creation - for i = 1, 6 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i) - end - - self.createButton({ - click_function = "drawEncounterCard", - function_owner = self, - position = {-1.84, 0, -0.65}, - rotation = {0, 80, 0}, - width = 265, - height = 190 - }) - - self.createButton({ - click_function = "drawChaosTokenButton", - function_owner = self, - position = {1.85, 0, -0.74}, - rotation = {0, -45, 0}, - width = 135, - height = 135 - }) - - self.createButton({ - label = "Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.84, 0.1, -0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) - - -- save state loading - local state = JSON.decode(saveState) - if state ~= nil then - playerColor = state.playerColor - activeInvestigatorId = state.activeInvestigatorId - isDrawButtonVisible = state.isDrawButtonVisible - end - - showDrawButton(isDrawButtonVisible) - math.randomseed(os.time()) - Wait.time(function() collisionEnabled = true end, 0.1) -end - ---------------------------------------------------------- --- utility functions ---------------------------------------------------------- - --- searches an area and optionally filters the result -function searchArea(origin, size, filter) - return searchLib.inArea(origin, self.getRotation(), size, filter) -end - --- finds all objects on the playmat and associated set aside zone. -function searchAroundSelf(filter) - local bounds = self.getBoundsNormalized() - -- Increase the width to cover the set aside zone - bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER - bounds.size.y = 1 - -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge - -- of the cast at the edge of the playmat - -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the - -- 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 - return searchArea(self.positionToWorld(localCenter), bounds.size, filter) -end - --- searches the area around the draw deck and discard pile -function searchDeckAndDiscardArea(filter) - local pos = self.positionToWorld(DECK_DISCARD_AREA.center) - local scale = self.getScale() - local size = { - x = DECK_DISCARD_AREA.size.x * scale.x, - y = DECK_DISCARD_AREA.size.y, - z = DECK_DISCARD_AREA.size.z * scale.z - } - return searchArea(pos, size, filter) -end - -function doNotReady(card) - return card.getVar("do_not_ready") or false -end - --- rounds a number to the specified amount of decimal places ----@param num number Initial value ----@param numDecimalPlaces number Amount of decimal places -function round(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) - return math.floor(num * mult + 0.5) / mult -end - ---------------------------------------------------------- --- Discard buttons ---------------------------------------------------------- - --- 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 obj.type == "Card" or obj.type == "Deck" then - if obj.hasTag("PlayerCard") then - deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) - else - deckLib.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 - chaosBagApi.returnChaosTokenToBag(obj) - -- don't touch locked objects (like the table etc.) - elseif not obj.getLock() then - ownedObjects.Trash.putObject(obj) - end - 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 handlerName = 'handler' .. number - 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, - function_owner = self, - position = {position[1], position[2], position[3] + 0.6}, - scale = {0.12, 0.12, 0.12}, - width = 900, - height = 350, - font_size = 220 - }) -end - ---------------------------------------------------------- --- Upkeep button ---------------------------------------------------------- - --- calls the Upkeep function with correct parameter -function doUpkeepFromHotkey(color) - doUpkeep(_, color) -end - -function doUpkeep(_, clickedByColor, isRightClick) - if isRightClick then - changeColor(clickedByColor) - return - end - - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or clickedByColor - - -- unexhaust cards in play zone, flip action tokens and find forcedLearning - local forcedLearning = false - local rot = self.getRotation() - for _, obj in ipairs(searchAroundSelf()) do - if obj.getDescription() == "Action Token" and obj.is_face_down then - obj.flip() - elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then - local cardMetadata = JSON.decode(obj.getGMNotes()) or {} - if not doNotReady(obj) then - local cardRotation = round(obj.getRotation().y, 0) - rot.y - local yRotDiff = 0 - - if cardRotation < 0 then - cardRotation = cardRotation + 360 - end - - -- rotate cards to the next multiple of 90° towards 0° - if cardRotation > 90 and cardRotation <= 180 then - yRotDiff = 90 - elseif cardRotation < 270 and cardRotation > 180 then - yRotDiff = 270 - end - - -- set correct rotation for face-down cards - rot.z = obj.is_face_down and 180 or 0 - obj.setRotation({rot.x, rot.y + yRotDiff, rot.z}) - end - if cardMetadata.id == "08031" then - forcedLearning = true - end - if cardMetadata.uses ~= nil then - tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) - end - end - end - - -- flip investigator mini-card and summoned servitor mini-card - -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs) - local miniId = string.match(activeInvestigatorId, ".....") .. "-m" - for _, obj in ipairs(getObjects()) do - if obj.type == "Card" and obj.is_face_down then - local notes = JSON.decode(obj.getGMNotes()) - if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then - obj.flip() - end - end - end - - -- gain a resource (or two if playing Jenny Barnes) - if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then - updateCounter({type = "ResourceCounter", modifier = 2}) - printToColor("Gaining 2 resources (Jenny)", messageColor) - else - updateCounter({type = "ResourceCounter", modifier = 1}) - end - - -- draw a card (with handling for Patrice and Forced Learning) - if activeInvestigatorId == "06005" then - if forcedLearning then - printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) - else - local handSize = #Player[playerColor].getHandObjects() - if handSize < 5 then - local cardsToDraw = 5 - handSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - drawCardsWithReshuffle(cardsToDraw) - end - end - elseif forcedLearning then - printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor) - drawCardsWithReshuffle(2) - elseif activeInvestigatorId == "89001" then - printToColor("Drawing 2 cards (Subject 5U-21)", messageColor) - drawCardsWithReshuffle(2) - else - drawCardsWithReshuffle(1) - end -end - --- function for "draw 1 button" (that can be added via option panel) -function doDrawOne(_, color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or color - drawCardsWithReshuffle(1) -end - --- draw X cards (shuffle discards if necessary) -function drawCardsWithReshuffle(numCards) - local deckAreaObjects = getDeckAreaObjects() - - -- Norman Withers handling - local harbinger = false - if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == "The Harbinger" then - harbinger = true - elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then - local cards = deckAreaObjects.draw.getObjects() - if cards[#cards].name == "The Harbinger" then - harbinger = true - end - end - - if harbinger then - printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor) - return - end - - local topCardDetected = false - if deckAreaObjects.topCard ~= nil then - deckAreaObjects.topCard.deal(1, playerColor) - topCardDetected = true - numCards = numCards - 1 - if numCards == 0 then - flipTopCardFromDeck() - return - end - end - - local deckSize = 1 - if deckAreaObjects.draw == nil then - deckSize = 0 - elseif deckAreaObjects.draw.type == "Deck" then - deckSize = #deckAreaObjects.draw.getObjects() - end - - if deckSize >= numCards then - drawCards(numCards) - -- flip top card again for Norman - if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then - flipTopCardFromDeck() - end - else - drawCards(deckSize) - if deckAreaObjects.discard ~= nil then - shuffleDiscardIntoDeck() - Wait.time(function() - drawCards(numCards - deckSize) - -- flip top card again for Norman - if topCardDetected and string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then - flipTopCardFromDeck() - end - end, 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - end -end - --- get the draw deck and discard pile objects and returns the references -function getDeckAreaObjects() - local deckAreaObjects = {} - for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do - if self.positionToLocal(object.getPosition()).z > 0.5 then - deckAreaObjects.discard = object - -- Norman Withers handling - elseif object.type == "Card" and not object.is_face_down then - deckAreaObjects.topCard = object - else - deckAreaObjects.draw = object - end - end - return deckAreaObjects -end - -function drawCards(numCards) - local deckAreaObjects = getDeckAreaObjects() - if deckAreaObjects.draw then - deckAreaObjects.draw.deal(numCards, playerColor) - end -end - -function shuffleDiscardIntoDeck() - local deckAreaObjects = getDeckAreaObjects() - if not deckAreaObjects.discard.is_face_down then - deckAreaObjects.discard.flip() - end - deckAreaObjects.discard.shuffle() - deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false) -end - --- utility function for Norman Withers to flip the top card to the revealed side -function flipTopCardFromDeck() - Wait.time(function() - local deckAreaObjects = getDeckAreaObjects() - if deckAreaObjects.topCard then - elseif deckAreaObjects.draw then - if deckAreaObjects.draw.type == "Card" then - deckAreaObjects.draw.flip() - else - -- get bounds to know the height of the deck - local bounds = deckAreaObjects.draw.getBounds() - local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0) - deckAreaObjects.draw.takeObject({ position = pos, flip = true }) - end - end - end, 0.1) -end - --- discard a random non-hidden card from hand -function doDiscardOne() - local hand = Player[playerColor].getHandObjects() - if #hand == 0 then - broadcastToAll("Cannot discard from empty hand!", "Red") - else - local choices = {} - for i = 1, #hand do - local notes = JSON.decode(hand[i].getGMNotes()) - if notes ~= nil then - if notes.hidden ~= true then - table.insert(choices, i) - end - else - table.insert(choices, i) - end - end - - if #choices == 0 then - broadcastToAll("Hidden cards can't be randomly discarded.", "Orange") - return - end - - -- get a random non-hidden card (from the "choices" table) - local num = math.random(1, #choices) - deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) - - local playerName = Player[playerColor].steam_name or playerColor - broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White") - end -end - ---------------------------------------------------------- --- color related functions ---------------------------------------------------------- - --- changes the player color -function changeColor(clickedByColor) - local colorList = { - "White", - "Brown", - "Red", - "Orange", - "Yellow", - "Green", - "Teal", - "Blue", - "Purple", - "Pink" - } - - -- remove existing colors from the list of choices - for _, existingColor in ipairs(Player.getAvailableColors()) do - for i, newColor in ipairs(colorList) do - if existingColor == newColor then - table.remove(colorList, i) - end - end - end - - -- show the option dialog for color selection to the player that triggered this - Player[clickedByColor].showOptionsDialog("Select a new color:", colorList, _, function(color) - -- update the color of the hand zone - local handZone = ownedObjects.HandZone - handZone.setValue(color) - - -- if the seated player clicked this, reseat him to the new color - if clickedByColor == playerColor then - navigationOverlayApi.copyVisibility(playerColor, color) - Player[playerColor].changeColor(color) - end - - -- update the internal variable - playerColor = color - end) -end - ---------------------------------------------------------- --- playmat token spawning ---------------------------------------------------------- - --- Finds all customizable cards in this play area and updates their metadata based on the selections --- on the matching upgrade sheet. --- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be --- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the --- number of customizable cards in play. -function syncAllCustomizableCards() - for _, card in ipairs(searchAroundSelf("isCard")) do - syncCustomizableMetadata(card) - end -end - -function syncCustomizableMetadata(card) - local cardMetadata = JSON.decode(card.getGMNotes()) or { } - if cardMetadata == nil or cardMetadata.customizations == nil then - return - end - for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do - local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { } - if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then - for i, customization in ipairs(cardMetadata.customizations) do - if customization.replaces ~= nil and customization.replaces.uses ~= nil then - -- Allowed use of call(), no APIs for individual cards - if upgradeSheet.call("isUpgradeActive", i) then - cardMetadata.uses = customization.replaces.uses - card.setGMNotes(JSON.encode(cardMetadata)) - else - -- TODO: Get the original metadata to restore it... maybe. This should only be - -- necessary in the very unlikely case that a user un-checks a previously-full upgrade - -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is - -- in place, so defer until it is - end - end - end - end - end -end - -function spawnTokensFor(object) - local extraUses = { } - if activeInvestigatorId == "03004" then - extraUses["Charge"] = 1 - end - - tokenManager.spawnForCard(object, extraUses) -end - -function onCollisionEnter(collisionInfo) - local object = collisionInfo.collision_object - - -- only continue if loading is completed - if not collisionEnabled then return end - - -- only continue for cards - if object.type ~= "Card" then return end - - -- detect if "Dream-Enhancing Serum" is placed - if object.getName() == "Dream-Enhancing Serum" then isDES = true end - - maybeUpdateActiveInvestigator(object) - syncCustomizableMetadata(object) - - local localCardPos = self.positionToLocal(object.getPosition()) - if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) - removeTokensFromObject(object) - elseif shouldSpawnTokens(object) then - spawnTokensFor(object) - end -end - --- detect if "Dream-Enhancing Serum" is removed -function onCollisionExit(collisionInfo) - if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end -end - --- checks if tokens should be spawned for the provided card -function shouldSpawnTokens(card) - if card.is_face_down then - return false - end - - local localCardPos = self.positionToLocal(card.getPosition()) - local metadata = JSON.decode(card.getGMNotes()) - - -- If no metadata we don't know the type, so only spawn in the main area - if metadata == nil then - return inArea(localCardPos, MAIN_PLAY_AREA) - end - - -- Spawn tokens for assets and events on the main area - if inArea(localCardPos, MAIN_PLAY_AREA) - and (metadata.type == "Asset" - or metadata.type == "Event") then - return true - end - - -- Spawn tokens for all encounter types in the threat area - if inArea(localCardPos, THREAT_AREA) - and (metadata.type == "Treachery" - or metadata.type == "Enemy" - or metadata.weakness) then - return true - end - - return false -end - -function onObjectEnterContainer(container, object) - if object.type ~= "Card" then return end - - local localCardPos = self.positionToLocal(object.getPosition()) - if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) - removeTokensFromObject(object) - end -end - --- removes tokens from the provided card/deck -function removeTokensFromObject(object) - if object.hasTag("CardThatSeals") then - local func = object.getVar("resetSealedTokens") -- check if function exists (it won't for older custom content) - if func ~= nil then - object.call("resetSealedTokens") - end - end - - for _, obj in ipairs(searchLib.onObject(object)) do - if tokenChecker.isChaosToken(obj) then - chaosBagApi.returnChaosTokenToBag(obj) - elseif obj.getGUID() ~= "4ee1f2" and -- table - obj ~= self and - obj.type ~= "Deck" and - obj.type ~= "Card" and - obj.memo ~= nil and - obj.getLock() == false and - obj.getDescription() ~= "Action Token" then - ownedObjects.Trash.putObject(obj) - end - end -end - ---------------------------------------------------------- --- investigator ID grabbing and skill tracker ---------------------------------------------------------- - -function maybeUpdateActiveInvestigator(card) - if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end - - local notes = JSON.decode(card.getGMNotes()) - local class - - if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then - if notes.id == activeInvestigatorId then return end - class = notes.class - activeInvestigatorId = notes.id - ownedObjects.InvestigatorSkillTracker.call("updateStats", { - notes.willpowerIcons, - notes.intellectIcons, - notes.combatIcons, - notes.agilityIcons - }) - elseif activeInvestigatorId ~= "00000" then - class = "Neutral" - activeInvestigatorId = "00000" - ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1}) - else - return - end - - -- change state of action tokens - local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1}) - local smallToken = nil - local STATE_TABLE = { - ["Guardian"] = 1, - ["Seeker"] = 2, - ["Rogue"] = 3, - ["Mystic"] = 4, - ["Survivor"] = 5, - ["Neutral"] = 6 - } - - for _, obj in ipairs(search) do - if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then - if obj.getScale().x < 0.4 then - smallToken = obj - else - setObjectState(obj, STATE_TABLE[class]) - end - end - end - - -- update the small token with special action for certain investigators - local SPECIAL_ACTIONS = { - ["04002"] = 8, -- Ursula Downs - ["01002"] = 9, -- Daisy Walker - ["01502"] = 9, -- Daisy Walker - ["01002-pb"] = 9, -- Daisy Walker - ["06003"] = 10, -- Tony Morgan - ["04003"] = 11, -- Finn Edwards - ["08016"] = 14 -- Bob Jenkins - } - - if smallToken ~= nil then - setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class]) - end -end - -function setObjectState(obj, stateId) - if obj.getStateId() ~= stateId then obj.setState(stateId) end -end - ---------------------------------------------------------- --- manipulation of owned objects ---------------------------------------------------------- - --- updates the specific owned counter ----@param param table Contains the information to update: ---- type: String Counter to target ---- newValue: Number Value to set the counter to ---- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier -function updateCounter(param) - local counter = ownedObjects[param.type] - if counter ~= nil then - counter.call("updateVal", param.newValue or (counter.getVar("val") + param.modifier)) - else - printToAll(param.type .. " for " .. matColor .. " could not be found.", "Yellow") - end -end - --- returns the resource counter amount ----@param type string Counter to target -function getCounterValue(type) - return ownedObjects[type].getVar("val") -end - --- set investigator skill tracker to "1, 1, 1, 1" -function resetSkillTracker() - local obj = ownedObjects.InvestigatorSkillTracker - if obj ~= nil then - obj.call("updateStats", { 1, 1, 1, 1 }) - else - printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow") - end -end - ---------------------------------------------------------- --- calls to 'Global' / functions for calls from outside ---------------------------------------------------------- - -function drawChaosTokenButton(_, _, isRightClick) - chaosBagApi.drawChaosToken(self, isRightClick) -end - -function drawEncounterCard(_, _, isRightClick) - mythosAreaApi.drawEncounterCard(self, isRightClick) -end - -function returnGlobalDiscardPosition() - return self.positionToWorld(DISCARD_PILE_POSITION) -end - --- Sets this playermat's draw 1 button to visible ----@param visible boolean Whether the draw 1 button should be visible -function showDrawButton(visible) - isDrawButtonVisible = visible - - -- create the "Draw 1" button - if isDrawButtonVisible then - self.createButton({ - label = "Draw 1", - click_function = "doDrawOne", - function_owner = self, - position = { 1.84, 0.1, -0.36 }, - scale = { 0.12, 0.12, 0.12 }, - width = 800, - height = 280, - font_size = 180 - }) - - -- remove the "Draw 1" button - else - local buttons = self.getButtons() - for i = 1, #buttons do - if buttons[i].label == "Draw 1" then - self.removeButton(buttons[i].index) - end - end - end -end - --- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues ----@param showCounter boolean Whether the clickable clue counter should be visible -function clickableClues(showCounter) - local clickerPos = ownedObjects.ClickableClueCounter.getPosition() - local clueCount = 0 - - -- move clue counters - local modY = showCounter and 0.525 or -0.525 - ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) - - if showCounter then - -- current clue count - clueCount = ownedObjects.ClueCounter.getVar("exposedValue") - - -- remove clues - ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) - - -- set value for clue clickers - ownedObjects.ClickableClueCounter.call("updateVal", clueCount) - else - -- current clue count - clueCount = ownedObjects.ClickableClueCounter.getVar("val") - - -- spawn clues - local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7}) - for i = 1, clueCount do - pos.y = pos.y + 0.045 * i - tokenManager.spawnToken(pos, "clue", self.getRotation()) - end - end -end - --- removes all clues (moving tokens to the trash and setting counters to 0) -function removeClues() - ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) - ownedObjects.ClickableClueCounter.call("updateVal", 0) -end - --- reports the clue count ----@param useClickableCounters boolean Controls which type of counter is getting checked -function getClueCount(useClickableCounters) - if useClickableCounters then - return ownedObjects.ClickableClueCounter.getVar("val") - else - return ownedObjects.ClueCounter.getVar("exposedValue") - end -end - --- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes --- is true, the main card slot snap points will only snap assets, while the investigator area point --- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards. ----@param matchTypes boolean Whether snap points should only snap for the matching card types. -function setLimitSnapsByType(matchTypes) - local snaps = self.getSnapPoints() - for i, snap in ipairs(snaps) do - local snapPos = snap.position - if inArea(snapPos, MAIN_PLAY_AREA) then - local snapTags = snaps[i].tags - if matchTypes then - if snapTags == nil then - snaps[i].tags = { "Asset" } - else - table.insert(snaps[i].tags, "Asset") - end - else - snaps[i].tags = nil - end - end - if inArea(snapPos, INVESTIGATOR_AREA) then - local snapTags = snaps[i].tags - if matchTypes then - if snapTags == nil then - snaps[i].tags = { "Investigator" } - else - table.insert(snaps[i].tags, "Investigator") - end - else - snaps[i].tags = nil - end - end - end - self.setSnapPoints(snaps) -end - --- Simple method to check if the given point is in a specified area. Local use only, ----@param point tts__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 --- bounds definition. ----@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) -end - --- called by custom data helpers to add player card data ----@param args table Contains only one entry, the GUID of the custom data helper -function updatePlayerCards(args) - local customDataHelper = getObjectFromGUID(args[1]) - local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") - tokenManager.addPlayerCardData(playerCardData) -end -end) -__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local DeckLib = {} - local searchLib = require("util/SearchLib") + local OptionPanelApi = {} - -- places a card/deck at a position or merges into an existing deck - ---@param obj tts__Object Object to move - ---@param pos table New position for the object - ---@param rot table New rotation for the object (optional) - DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot) - if obj == nil or pos == nil then return end - - -- search the new position for existing card/deck - local searchResult = searchLib.atPosition(pos, "isCardOrDeck") - - -- get new position - local newPos - local offset = 0.5 - if #searchResult == 1 then - local bounds = searchResult[1].getBounds() - newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) - else - newPos = Vector(pos) + Vector(0, offset, 0) - end - - -- allow moving the objects smoothly out of the hand - obj.use_hands = false - - if rot then - obj.setRotationSmooth(rot, false, true) - end - obj.setPositionSmooth(newPos, false, true) - - -- continue if the card stops smooth moving - Wait.condition( - function() - obj.use_hands = true - -- this avoids a TTS bug that merges unrelated cards that are not resting - 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 - end, - function() return not obj.isSmoothMoving() end, 3) + -- loads saved options + ---@param options table Set a new state for the option table + OptionPanelApi.loadSettings = function(options) + return Global.call("loadSettings", options) end - return DeckLib -end -end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } - - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] - end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) - - -- filtering the result - local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) - end - end - return objList + ---@return any: Table of option panel state + OptionPanelApi.getOptions = function() + return Global.getTable("optionPanel") end - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) - end - - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) - end - - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib + return OptionPanelApi end end) return __bundle_require("__root") diff --git a/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.yaml b/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.yaml index 89be9c75e..d10f51b48 100644 --- a/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.yaml +++ b/unpacked/Custom_Tile Playermat 2 Orange bd0ff4.yaml @@ -8,61 +8,61 @@ AttachedSnapPoints: y: 0.1 z: 0.12 Tags: - - ActionToken + - UniversalToken - Position: x: -0.86 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1 + x: -1.03 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.18 + x: -1.2 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.36 + x: -1.37 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -0.63 + x: -1.54 y: 0.1 - z: 0.55 + z: -0.28 + Tags: + - UniversalToken +- Position: + x: 1.76 + y: 0.1 + z: 0.04 Tags: - Asset - Position: - x: -0.62 + x: 1.37 y: 0.1 - z: 0.02 + z: 0.04 Tags: - Asset - Position: - x: -0.18 + x: 0.98 + y: 0.1 + z: 0.04 + Tags: + - Asset +- Position: + x: 0.6 y: 0.1 z: 0.03 Tags: - Asset -- Position: - x: -0.17 - y: 0.1 - z: 0.55 - Tags: - - Asset -- Position: - x: 0.21 - y: 0.1 - z: 0.56 - Tags: - - Asset - Position: x: 0.22 y: 0.1 @@ -70,39 +70,15 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 0.6 + x: -0.18 y: 0.1 z: 0.03 Tags: - Asset - Position: - x: 0.6 + x: -0.62 y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.56 + z: 0.02 Tags: - Asset - Position: @@ -112,9 +88,39 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 1.76 + x: 1.37 y: 0.1 - z: 0.04 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.98 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.6 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.21 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: -0.17 + y: 0.1 + z: 0.55 + Tags: + - Asset +- Position: + x: -0.63 + y: 0.1 + z: 0.55 Tags: - Asset - Position: @@ -208,7 +214,7 @@ CustomImage: Type: 3 ImageScalar: 1 ImageSecondaryURL: '' - ImageURL: http://cloud-3.steamusercontent.com/ugc/2037357630681963618/E7271737B19CE0BFAAA382BEEEF497FE3E06ECC1/ + ImageURL: http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/ WidthScale: 0 Description: '' DragSelectable: true @@ -222,7 +228,8 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Tile Playermat 2 Orange bd0ff4.ttslua' -LuaScriptState: '{"activeInvestigatorId":"00000","isDrawButtonVisible":false,"playerColor":"Orange"}' +LuaScriptState: '{"activeInvestigatorClass":"Neutral","activeInvestigatorId":"00000","isClassTextureEnabled":true,"isDrawButtonVisible":false,"playerColor":"Orange","slotData":["any","any","any","Tarot","Hand + (left)","Hand (right)","Ally","any","any","any","Accessory","Arcane","Arcane","Body"]}' MeasureMovement: false Memo: Orange Name: Custom_Tile diff --git a/unpacked/Custom_Tile Playermat 3 Green 383d8b.ttslua b/unpacked/Custom_Tile Playermat 3 Green 383d8b.ttslua index 9f92b3fcd..5c03712f2 100644 --- a/unpacked/Custom_Tile Playermat 3 Green 383d8b.ttslua +++ b/unpacked/Custom_Tile Playermat 3 Green 383d8b.ttslua @@ -41,140 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playermat/Playmat") -end) -__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local ChaosBagApi = {} - - -- respawns the chaos bag with a new state of tokens - ---@param tokenList table List of chaos token ids - ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getChaosBagState = function() - local chaosBagContentsCatcher = Global.call("getChaosBagState") - local chaosBagContents = {} - for _, v in ipairs(chaosBagContentsCatcher) do - table.insert(chaosBagContents, v) - end - return chaosBagContents - end - - -- checks scripting zone for chaos bag (also called by a lot of objects!) - ChaosBagApi.findChaosBag = function() - return Global.call("findChaosBag") - end - - -- returns a table of object references to the tokens in play (does not include sealed tokens!) - ChaosBagApi.getTokensInPlay = function() - return Global.call("getChaosTokensinPlay") - end - - -- returns all sealed tokens on cards to the chaos bag - ---@param playerColor string Color of the player to show the broadcast to - ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) - end - - -- returns all drawn tokens to the chaos bag - ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") - end - - -- removes the specified chaos token from the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) - end - - -- returns a chaos token to the bag and calls all relevant functions - ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) - end - - -- spawns the specified chaos token and puts it into the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) - end - - -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens - -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the - -- contents of the bag should check this method before doing so. - -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. - ChaosBagApi.canTouchChaosTokens = function() - return Global.call("canTouchChaosTokens") - end - - -- called by playermats (by the "Draw chaos token" button) - ---@param mat tts__Object Playermat that triggered this - ---@param drawAdditional boolean Controls whether additional tokens should be drawn - ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag - ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getIdUrlMap = function() - return Global.getTable("ID_URL_MAP") - end - - return ChaosBagApi -end -end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) __bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local MythosAreaApi = {} @@ -188,24 +54,24 @@ do MythosAreaApi.returnTokenData = function() return getMythosArea().call("returnTokenData") end - + ---@return any: Object reference to the encounter deck MythosAreaApi.getEncounterDeck = function() return getMythosArea().call("getEncounterDeck") end - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) end -- reshuffle the encounter deck MythosAreaApi.reshuffleEncounterDeck = function() getMythosArea().call("reshuffleEncounterDeck") end - + return MythosAreaApi end end) @@ -247,6 +113,130 @@ do return NavigationOverlayApi end end) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } + + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList + end + + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) + end + + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) __bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local OptionPanelApi = {} @@ -265,166 +255,12 @@ do return OptionPanelApi end end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local PlayAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") - end - - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") - end - - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") - end - - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) - end - - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) - end - - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) - end - - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) - end - - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) - end - - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) - end - - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) - end - - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) - end - - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) - end - - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi -end -end) -__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local CHAOS_TOKEN_NAMES = { - ["Elder Sign"] = true, - ["+1"] = true, - ["0"] = true, - ["-1"] = true, - ["-2"] = true, - ["-3"] = true, - ["-4"] = true, - ["-5"] = true, - ["-6"] = true, - ["-7"] = true, - ["-8"] = true, - ["Skull"] = true, - ["Cultist"] = true, - ["Tablet"] = true, - ["Elder Thing"] = true, - ["Auto-fail"] = true, - ["Bless"] = true, - ["Curse"] = true, - ["Frost"] = true - } - - local TokenChecker = {} - - -- returns true if the passed object is a chaos token (by name) - TokenChecker.isChaosToken = function(obj) - if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then - return true - else - return false - end - end - - return TokenChecker -end -end) __bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules) do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -555,13 +391,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -576,11 +412,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -595,18 +431,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -618,11 +457,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -632,7 +470,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -658,16 +500,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -677,9 +519,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -715,21 +556,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -768,7 +601,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -778,11 +611,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -801,7 +634,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -818,7 +651,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -829,7 +662,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -902,21 +735,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -948,156 +776,154 @@ do return TokenManager end end) -__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local TokenSpawnTracker = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getSpawnTracker() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker") - end - - TokenSpawnTracker.hasSpawnedTokens = function(cardGuid) - return getSpawnTracker().call("hasSpawnedTokens", cardGuid) - end - - TokenSpawnTracker.markTokensSpawned = function(cardGuid) - return getSpawnTracker().call("markTokensSpawned", cardGuid) - end - - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) - end - - TokenSpawnTracker.resetAllAssetAndEvents = function() - return getSpawnTracker().call("resetAllAssetAndEvents") - end - - TokenSpawnTracker.resetAllLocations = function() - return getSpawnTracker().call("resetAllLocations") - end - - TokenSpawnTracker.resetAll = function() - return getSpawnTracker().call("resetAll") - end - - return TokenSpawnTracker -end -end) -__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules) -local chaosBagApi = require("chaosbag/ChaosBagApi") -local deckLib = require("util/DeckLib") -local guidReferenceApi = require("core/GUIDReferenceApi") -local mythosAreaApi = require("core/MythosAreaApi") -local navigationOverlayApi = require("core/NavigationOverlayApi") -local searchLib = require("util/SearchLib") -local tokenChecker = require("core/token/TokenChecker") -local tokenManager = require("core/token/TokenManager") +__bundle_register("playermat/Playermat", function(require, _LOADED, __bundle_register, __bundle_modules) +local chaosBagApi = require("chaosbag/ChaosBagApi") +local deckLib = require("util/DeckLib") +local guidReferenceApi = require("core/GUIDReferenceApi") +local mythosAreaApi = require("core/MythosAreaApi") +local navigationOverlayApi = require("core/NavigationOverlayApi") +local searchLib = require("util/SearchLib") +local tokenChecker = require("core/token/TokenChecker") +local tokenManager = require("core/token/TokenManager") +local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") -- we use this to turn off collision handling until onLoad() is complete -local collisionEnabled = false +local collisionEnabled = false +local currentlyEditingSlots = false -- x-Values for discard buttons -local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91} +local DISCARD_BUTTON_X_START = -1.365 +local DISCARD_BUTTON_X_OFFSET = 0.455 local SEARCH_AROUND_SELF_X_BUFFER = 8 +local SEARCH_AROUND_SELF_Z_BUFFER = 1.75 -- defined areas for object searching -local MAIN_PLAY_AREA = { - upperLeft = { - x = 1.98, - z = 0.736 - }, - lowerRight = { - x = -0.79, - z = -0.39 - } +local MAIN_PLAY_AREA = { + upperLeft = { x = 1.98, z = 0.736 }, + lowerRight = { x = -0.79, z = -0.39 } } -local INVESTIGATOR_AREA = { - upperLeft = { - x = -1.084, - z = 0.06517 - }, - lowerRight = { - x = -1.258, - z = -0.0805 - } +local INVESTIGATOR_AREA = { + upperLeft = { x = -1.084, z = 0.06517 }, + lowerRight = { x = -1.258, z = -0.0805 } } -local THREAT_AREA = { - upperLeft = { - x = 1.53, - z = -0.34 - }, - lowerRight = { - x = -1.13, - z = -0.92 - } +local THREAT_AREA = { + upperLeft = { x = 1.53, z = -0.34 }, + lowerRight = { x = -1.13, z = -0.92 } } -local DECK_DISCARD_AREA = { - upperLeft = { - x = -1.62, - z = 0.855 - }, - lowerRight = { - x = -2.02, - z = -0.245 - }, - center = { - x = -1.82, - y = 0.5, - z = 0.305 - }, - size = { - x = 0.4, - y = 3, - z = 1.1 - } +local DECK_DISCARD_AREA = { + upperLeft = { x = -1.62, z = 0.855 }, + lowerRight = { x = -2.02, z = -0.245 }, + center = { x = -1.82, y = 0.5, z = 0.305 }, + size = { x = 0.4, y = 3, z = 1.1 } } --- local position of draw and discard pile -local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } -local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +-- local positions +local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } +local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +local DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 } -- global position of encounter discard pile -local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38} +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" +-- used for the buttons on the right side of the playermat +-- starts off with the data for the "Upkeep" button and will then be changed +local buttonParameters = { + label = "Upkeep", + click_function = "doUpkeep", + tooltip = "Right-click to change color", + function_owner = self, + position = { x = 1.82, y = 0.1, z = -0.45 }, + scale = { 0.12, 0.12, 0.12 }, + width = 1000, + height = 280, + font_size = 180 +} + +-- table of texture URLs +local nameToTexture = { + Guardian = "http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/", + Mystic = "http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/", + Neutral = "http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/", + Rogue = "http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/", + Seeker = "http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/", + Survivor = "http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/" +} + +-- translation table for slot names to characters for special font +local slotNameToChar = { + ["any"] = "", + ["Accessory"] = "C", + ["Ally"] = "E", + ["Arcane"] = "G", + ["Body"] = "K", + ["Hand (right)"] = "M", + ["Hand (left)"] = "M", + ["Hand x2"] = "N", + ["Tarot"] = "A" +} + +-- slot symbol for the respective slot (from top left to bottom right) - intentionally global! +slotData = {} +local defaultSlotData = { + -- 1st row + "any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally", + + -- 2nd row + "any", "any", "any", "Accessory", "Arcane", "Arcane", "Body" +} + +-- global variables for access +activeInvestigatorClass = "Neutral" +activeInvestigatorId = "00000" +hasDES = false + +local isClassTextureEnabled = true +local isDrawButtonVisible = false -- table of type-object reference pairs of all owned objects -local ownedObjects = {} -local matColor = self.getMemo() - --- variable to track the status of the "Show Draw Button" option -local isDrawButtonVisible = false - --- global variable to report "Dream-Enhancing Serum" status -isDES = false +local ownedObjects = {} +local matColor = self.getMemo() function onSave() return JSON.encode({ - playerColor = playerColor, + activeInvestigatorClass = activeInvestigatorClass, activeInvestigatorId = activeInvestigatorId, - isDrawButtonVisible = isDrawButtonVisible + isClassTextureEnabled = isClassTextureEnabled, + isDrawButtonVisible = isDrawButtonVisible, + playerColor = playerColor, + slotData = slotData }) end -function onLoad(saveState) +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) + activeInvestigatorClass = loadedData.activeInvestigatorClass + activeInvestigatorId = loadedData.activeInvestigatorId + isClassTextureEnabled = loadedData.isClassTextureEnabled + isDrawButtonVisible = loadedData.isDrawButtonVisible + playerColor = loadedData.playerColor + slotData = loadedData.slotData + end + + updateMessageColor(playerColor) + self.interactable = false -- get object references to owned objects ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) - -- button creation + -- discard button creation for i = 1, 6 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i) + makeDiscardButton(i) end self.createButton({ click_function = "drawEncounterCard", function_owner = self, - position = {-1.84, 0, -0.65}, - rotation = {0, 80, 0}, + position = { -1.84, 0, -0.65 }, + rotation = { 0, 80, 0 }, width = 265, height = 190 }) @@ -1105,32 +931,24 @@ function onLoad(saveState) self.createButton({ click_function = "drawChaosTokenButton", function_owner = self, - position = {1.85, 0, -0.74}, - rotation = {0, -45, 0}, + position = { 1.85, 0, -0.74 }, + rotation = { 0, -45, 0 }, width = 135, height = 135 }) - self.createButton({ - label = "Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.84, 0.1, -0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) + -- Upkeep button: can use the default parameters for this + self.createButton(buttonParameters) - -- save state loading - local state = JSON.decode(saveState) - if state ~= nil then - playerColor = state.playerColor - activeInvestigatorId = state.activeInvestigatorId - isDrawButtonVisible = state.isDrawButtonVisible - end + -- Slot editing button: modified default data + buttonParameters.label = "Edit Slots" + buttonParameters.click_function = "toggleSlotEditing" + buttonParameters.tooltip = "Right-click to reset slot symbols" + buttonParameters.position.z = 0.92 + self.createButton(buttonParameters) showDrawButton(isDrawButtonVisible) + redrawSlotSymbols() math.randomseed(os.time()) Wait.time(function() collisionEnabled = true end, 0.1) end @@ -1144,19 +962,25 @@ function searchArea(origin, size, filter) return searchLib.inArea(origin, self.getRotation(), size, filter) end --- finds all objects on the playmat and associated set aside zone. +-- finds all objects on the playermat and associated set aside zone. function searchAroundSelf(filter) + local scale = self.getScale() local bounds = self.getBoundsNormalized() + -- Increase the width to cover the set aside zone bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER bounds.size.y = 1 - -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge - -- of the cast at the edge of the playmat - -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the - -- table position of the playmat + bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER + + -- 'setAsideDirection' accounts for the set aside zone being on the left or right, + -- depending on the table position of the playermat local setAsideDirection = bounds.center.z > 0 and 1 or -1 + + -- Since the cast is centered on the position, shift left or right to keep + -- the non-set aside edge of the cast at the edge of the playermat 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 / scale.x + localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z return searchArea(self.positionToWorld(localCenter), bounds.size, filter) end @@ -1166,24 +990,39 @@ function searchDeckAndDiscardArea(filter) local scale = self.getScale() local size = { x = DECK_DISCARD_AREA.size.x * scale.x, - y = DECK_DISCARD_AREA.size.y, + y = DECK_DISCARD_AREA.size.y, z = DECK_DISCARD_AREA.size.z * scale.z } return searchArea(pos, size, filter) end -function doNotReady(card) - return card.getVar("do_not_ready") or false -end - -- rounds a number to the specified amount of decimal places ---@param num number Initial value ---@param numDecimalPlaces number Amount of decimal places +---@return number: rounded number function round(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) + local mult = 10 ^ (numDecimalPlaces or 0) return math.floor(num * mult + 0.5) / mult end +-- edits the label of a button +---@param oldLabel string Old label of the button +---@param newLabel string New label of the button +function editButtonLabel(oldLabel, newLabel) + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == oldLabel then + self.editButton({ index = buttons[i].index, label = newLabel }) + end + end +end + +-- updates the internal "messageColor" which is used for print/broadcast statements if no player is seated +---@param clickedByColor string Colorstring of player who clicked a button +function updateMessageColor(clickedByColor) + messageColor = Player[playerColor].seated and playerColor or clickedByColor +end + --------------------------------------------------------- -- Discard buttons --------------------------------------------------------- @@ -1196,25 +1035,27 @@ function discardListOfObjects(objList) if obj.hasTag("PlayerCard") then deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) else - deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, {x = 0, y = -90, z = 0}) + deckLib.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 - chaosBagApi.returnChaosTokenToBag(obj) - -- don't touch locked objects (like the table etc.) - elseif not obj.getLock() then + -- put chaos tokens back into bag (e.g. Unrelenting) + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif not obj.getLock() and not obj.hasTag("DontDiscard") then + -- don't touch locked objects (like the table etc.) or specific objects (like key tokens) ownedObjects.Trash.putObject(obj) end 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 handlerName = 'handler' .. number +-- build a discard button to discard from searchPosition +---@param id number Index of the discard button (from left to right, must be unique) +function makeDiscardButton(id) + local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET + local position = { xValue, 0.1, -0.94 } + local searchPosition = { -position[1], position[2], position[3] + 0.32 } + local handlerName = 'handler' .. id self.setVar(handlerName, function() - local cardSizeSearch = {2, 1, 3.2} + local cardSizeSearch = { 2, 1, 3.2 } local globalSearchPosition = self.positionToWorld(searchPosition) local searchResult = searchArea(globalSearchPosition, cardSizeSearch) return discardListOfObjects(searchResult) @@ -1224,7 +1065,7 @@ function makeDiscardButton(xValue, number) click_function = handlerName, function_owner = self, position = {position[1], position[2], position[3] + 0.6}, - scale = {0.12, 0.12, 0.12}, + scale = { 0.12, 0.12, 0.12 }, width = 900, height = 350, font_size = 220 @@ -1236,8 +1077,8 @@ end --------------------------------------------------------- -- calls the Upkeep function with correct parameter -function doUpkeepFromHotkey(color) - doUpkeep(_, color) +function doUpkeepFromHotkey(clickedByColor) + doUpkeep(_, clickedByColor) end function doUpkeep(_, clickedByColor, isRightClick) @@ -1246,18 +1087,20 @@ function doUpkeep(_, clickedByColor, isRightClick) return end - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or clickedByColor + updateMessageColor(clickedByColor) - -- unexhaust cards in play zone, flip action tokens and find forcedLearning + -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum + checkForDES() local forcedLearning = false local rot = self.getRotation() for _, obj in ipairs(searchAroundSelf()) do - if obj.getDescription() == "Action Token" and obj.is_face_down then + if obj.hasTag("Temporary") == true then + discardListOfObjects({ obj }) + elseif obj.hasTag("UniversalToken") == true and obj.is_face_down then obj.flip() elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then local cardMetadata = JSON.decode(obj.getGMNotes()) or {} - if not doNotReady(obj) then + if not (obj.getVar("do_not_ready") or obj.hasTag("DoNotReady")) then local cardRotation = round(obj.getRotation().y, 0) - rot.y local yRotDiff = 0 @@ -1274,14 +1117,26 @@ function doUpkeep(_, clickedByColor, isRightClick) -- set correct rotation for face-down cards rot.z = obj.is_face_down and 180 or 0 - obj.setRotation({rot.x, rot.y + yRotDiff, rot.z}) + obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z }) end + + -- detect Forced Learning to handle card drawing accordingly if cardMetadata.id == "08031" then forcedLearning = true end - if cardMetadata.uses ~= nil then + + -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile) + if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x > -1 then tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) end + elseif obj.type == "Deck" and forcedLearning == false then + -- check decks for forced learning + for _, deepObj in ipairs(obj.getObjects()) do + local cardMetadata = JSON.decode(deepObj.gm_notes) or {} + if cardMetadata.id == "08031" then + forcedLearning = true + end + end end end @@ -1299,22 +1154,44 @@ function doUpkeep(_, clickedByColor, isRightClick) -- gain a resource (or two if playing Jenny Barnes) if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then - updateCounter({type = "ResourceCounter", modifier = 2}) + updateCounter({ type = "ResourceCounter", modifier = 2 }) printToColor("Gaining 2 resources (Jenny)", messageColor) else - updateCounter({type = "ResourceCounter", modifier = 1}) + updateCounter({ type = "ResourceCounter", modifier = 1 }) end -- draw a card (with handling for Patrice and Forced Learning) if activeInvestigatorId == "06005" then if forcedLearning then - printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) + printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?" + .. " Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) else - local handSize = #Player[playerColor].getHandObjects() - if handSize < 5 then - local cardsToDraw = 5 - handSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - drawCardsWithReshuffle(cardsToDraw) + -- discards all non-weakness and non-hidden cards from hand first + local handCards = Player[playerColor].getHandObjects() + local cardsToDiscard = {} + + for _, card in ipairs(handCards) do + local md = JSON.decode(card.getGMNotes()) + if card.type == "Card" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= "52020") then + table.insert(cardsToDiscard, card) + end + end + + -- perform discarding 1 by 1 + local pos = returnGlobalDiscardPosition() + deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation()) + + -- draw up to 5 cards + local cardsToDraw = 5 - #handCards + #cardsToDiscard + if cardsToDraw > 0 then + printToColor("Discarding " .. #cardsToDiscard .. " and drawing " .. cardsToDraw .. " card(s). (Patrice)", messageColor) + + -- add some time if there are any cards to discard + local k = 0 + if #cardsToDiscard > 0 then + k = 0.8 + (#cardsToDiscard * 0.1) + end + Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k) end end elseif forcedLearning then @@ -1328,14 +1205,14 @@ function doUpkeep(_, clickedByColor, isRightClick) end end --- function for "draw 1 button" (that can be added via option panel) -function doDrawOne(_, color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or color +-- click function for "draw 1 button" (that can be added via option panel) +function doDrawOne(_, clickedByColor) + updateMessageColor(clickedByColor) drawCardsWithReshuffle(1) end --- draw X cards (shuffle discards if necessary) +-- draws the specified amount of cards (and shuffles the discard if necessary) +---@param numCards number Number of cards to draw function drawCardsWithReshuffle(numCards) local deckAreaObjects = getDeckAreaObjects() @@ -1396,12 +1273,13 @@ function drawCardsWithReshuffle(numCards) end -- get the draw deck and discard pile objects and returns the references +---@return table: string-indexed table with references to the found objects function getDeckAreaObjects() local deckAreaObjects = {} for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do if self.positionToLocal(object.getPosition()).z > 0.5 then deckAreaObjects.discard = object - -- Norman Withers handling + -- Norman Withers handling elseif object.type == "Card" and not object.is_face_down then deckAreaObjects.topCard = object else @@ -1411,6 +1289,8 @@ function getDeckAreaObjects() return deckAreaObjects end +-- draws the specified number of cards (reshuffling of discard pile is handled separately) +---@param numCards number Number of cards to draw function drawCards(numCards) local deckAreaObjects = getDeckAreaObjects() if deckAreaObjects.draw then @@ -1449,31 +1329,211 @@ end function doDiscardOne() local hand = Player[playerColor].getHandObjects() if #hand == 0 then - broadcastToAll("Cannot discard from empty hand!", "Red") + broadcastToColor("Cannot discard from empty hand!", messageColor, "Red") else local choices = {} - for i = 1, #hand do - local notes = JSON.decode(hand[i].getGMNotes()) - if notes ~= nil then - if notes.hidden ~= true then + local hiddenCards = {} + local missingMetadataCards = {} + for i, handObj in ipairs(hand) do + if handObj.type == "Card" then + -- get a name for the card or use the index if unnamed + local name = handObj.getName() + if name == "" then + name = "Card " .. i + end + + -- check card for metadata + local md = JSON.decode(handObj.getGMNotes()) + if md == nil then + table.insert(missingMetadataCards, name) + elseif md.hidden or md.id == "52020" then + table.insert(hiddenCards, name) + else table.insert(choices, i) end - else - table.insert(choices, i) end end + -- print message with hidden cards + if #hiddenCards > 0 then + local cardList = concatenateListOfStrings(hiddenCards) + printToColor("Excluded (hidden): " .. cardList, messageColor) + end + + -- print message with missing metadata cards + if #missingMetadataCards > 0 then + local cardList = concatenateListOfStrings(missingMetadataCards) + printToColor("Excluded (missing data): " .. cardList, messageColor) + end + if #choices == 0 then - broadcastToAll("Hidden cards can't be randomly discarded.", "Orange") + broadcastToColor("Didn't find any eligible cards for random discarding.", messageColor, "Orange") return end - -- get a random non-hidden card (from the "choices" table) + -- get a random eligible card (from the "choices" table) local num = math.random(1, #choices) deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) + broadcastToAll(getColoredName(playerColor) .. " randomly discarded card " + .. choices[num] .. "/" .. #hand .. ".", "White") + end +end - local playerName = Player[playerColor].steam_name or playerColor - broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White") +function concatenateListOfStrings(list) + local cardList + for _, cardName in ipairs(list) do + if not cardList then + cardList = "" + else + cardList = cardList .. ", " + end + cardList = cardList .. cardName + end + return cardList +end + +-- checks if DES is present +function checkForDES() + hasDES = false + for _, obj in ipairs(searchAroundSelf()) do + if obj.type == "Card" then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + + -- position is used to exclude deck / discard + local cardPos = self.positionToLocal(obj.getPosition()) + if cardMetadata.id == "06159" and cardPos.x > -1 then + hasDES = true + break + end + end + end +end + +--------------------------------------------------------- +-- slot symbol displaying +--------------------------------------------------------- + +-- this will redraw the XML for the slot symbols based on the slotData table +function redrawSlotSymbols() + local xml = {} + local snapId = 0 + + -- use the snap point positions in the main play area for positions + for _, snap in ipairs(self.getSnapPoints()) do + if inArea(snap.position, MAIN_PLAY_AREA) then + snapId = snapId + 1 + local slotName = slotData[snapId] + + -- conversion from regular coordinates to XML + local x = snap.position.x * 100 + local y = snap.position.z * 100 + + -- XML for a single slot (panel with text in the special font) + local slotXML = { + tag = "Panel", + attributes = { + id = "slotPanel" .. snapId, + scale = "0.1 0.1 1", + width = "175", + height = "175", + position = x .. " " .. y .. " -11" + }, + children = { + { + tag = "Text", + attributes = { + id = "slot" .. snapId, + rotation = getSlotRotation(slotName), + fontSize = "145", + font = "font_arkhamicons", + color = "#414141CB", + text = slotNameToChar[slotName] + } + } + } + } + table.insert(xml, slotXML) + end + end + + self.UI.setXmlTable(xml) +end + +-- toggle the "slot editing mode" +function toggleSlotEditing(_, clickedByColor, isRightClick) + if isRightClick then + resetSlotSymbols() + return + end + + updateMessageColor(clickedByColor) + + -- toggle internal variable + currentlyEditingSlots = not currentlyEditingSlots + + if currentlyEditingSlots then + editButtonLabel("Edit Slots", "Stop editing") + broadcastToColor("Click on a slot symbol (or an empty slot) to edit it.", messageColor, "Orange") + addClickFunctionToSlots() + else + editButtonLabel("Stop editing", "Edit Slots") + redrawSlotSymbols() + end +end + +-- click function for slot symbols during the "slot editing mode" +function slotClickfunction(player, _, id) + local slotIndex = id:gsub("slotPanel", "") + slotIndex = tonumber(slotIndex) + + -- make a list of the table keys as options for the dialog box + local slotNames = {} + for slotName, _ in pairs(slotNameToChar) do + table.insert(slotNames, slotName) + end + + -- prompt player to choose symbol + player.showOptionsDialog("Choose Slot Symbol", slotNames, slotData[slotIndex], + function(chosenSlotName) + slotData[slotIndex] = chosenSlotName + + -- update slot symbol + self.UI.setAttribute("slot" .. slotIndex, "text", slotNameToChar[chosenSlotName]) + + -- update slot rotation + self.UI.setAttribute("slot" .. slotIndex, "rotation", getSlotRotation(chosenSlotName)) + end + ) +end + +-- helper function to rotate the left hand +function getSlotRotation(slotName) + if slotName == "Hand (left)" then + return "0 180 180" + else + return "0 0 180" + end +end + +-- reset the slot symbols by making a deep copy of the default data and redrawing +function resetSlotSymbols() + slotData = {} + for _, slotName in ipairs(defaultSlotData) do + table.insert(slotData, slotName) + end + + redrawSlotSymbols() + + -- need to re-add the click functions if currently in edit mode + if currentlyEditingSlots then + addClickFunctionToSlots() + end +end + +-- enables the click functions for editing +function addClickFunctionToSlots() + for i = 1, #slotData do + self.UI.setAttribute("slotPanel" .. i, "onClick", "slotClickfunction") end end @@ -1483,23 +1543,12 @@ end -- changes the player color function changeColor(clickedByColor) - local colorList = { - "White", - "Brown", - "Red", - "Orange", - "Yellow", - "Green", - "Teal", - "Blue", - "Purple", - "Pink" - } + local colorList = Player.getColors() -- remove existing colors from the list of choices for _, existingColor in ipairs(Player.getAvailableColors()) do for i, newColor in ipairs(colorList) do - if existingColor == newColor then + if existingColor == newColor or newColor == "Black" or newColor == "Grey" then table.remove(colorList, i) end end @@ -1523,7 +1572,7 @@ function changeColor(clickedByColor) end --------------------------------------------------------- --- playmat token spawning +-- playermat token spawning --------------------------------------------------------- -- Finds all customizable cards in this play area and updates their metadata based on the selections @@ -1538,16 +1587,14 @@ function syncAllCustomizableCards() end function syncCustomizableMetadata(card) - local cardMetadata = JSON.decode(card.getGMNotes()) or { } - if cardMetadata == nil or cardMetadata.customizations == nil then - return - end + local cardMetadata = JSON.decode(card.getGMNotes()) or {} + if cardMetadata == nil or cardMetadata.customizations == nil then return end + for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do - local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { } + local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {} if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then for i, customization in ipairs(cardMetadata.customizations) do if customization.replaces ~= nil and customization.replaces.uses ~= nil then - -- Allowed use of call(), no APIs for individual cards if upgradeSheet.call("isUpgradeActive", i) then cardMetadata.uses = customization.replaces.uses card.setGMNotes(JSON.encode(cardMetadata)) @@ -1564,7 +1611,7 @@ function syncCustomizableMetadata(card) end function spawnTokensFor(object) - local extraUses = { } + local extraUses = {} if activeInvestigatorId == "03004" then extraUses["Charge"] = 1 end @@ -1581,26 +1628,18 @@ function onCollisionEnter(collisionInfo) -- only continue for cards if object.type ~= "Card" then return end - -- detect if "Dream-Enhancing Serum" is placed - if object.getName() == "Dream-Enhancing Serum" then isDES = true end - maybeUpdateActiveInvestigator(object) syncCustomizableMetadata(object) local localCardPos = self.positionToLocal(object.getPosition()) if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) + tokenSpawnTrackerApi.resetTokensSpawned(object) removeTokensFromObject(object) elseif shouldSpawnTokens(object) then spawnTokensFor(object) end end --- detect if "Dream-Enhancing Serum" is removed -function onCollisionExit(collisionInfo) - if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end -end - -- checks if tokens should be spawned for the provided card function shouldSpawnTokens(card) if card.is_face_down then @@ -1618,15 +1657,15 @@ function shouldSpawnTokens(card) -- Spawn tokens for assets and events on the main area if inArea(localCardPos, MAIN_PLAY_AREA) and (metadata.type == "Asset" - or metadata.type == "Event") then + or metadata.type == "Event") then return true end -- Spawn tokens for all encounter types in the threat area if inArea(localCardPos, THREAT_AREA) and (metadata.type == "Treachery" - or metadata.type == "Enemy" - or metadata.weakness) then + or metadata.type == "Enemy" + or metadata.weakness) then return true end @@ -1638,7 +1677,7 @@ function onObjectEnterContainer(container, object) local localCardPos = self.positionToLocal(object.getPosition()) if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) + tokenSpawnTrackerApi.resetTokensSpawned(object) removeTokensFromObject(object) end end @@ -1654,14 +1693,13 @@ function removeTokensFromObject(object) for _, obj in ipairs(searchLib.onObject(object)) do if tokenChecker.isChaosToken(obj) then - chaosBagApi.returnChaosTokenToBag(obj) + chaosBagApi.returnChaosTokenToBag(obj, false) elseif obj.getGUID() ~= "4ee1f2" and -- table obj ~= self and obj.type ~= "Deck" and obj.type ~= "Card" and obj.memo ~= nil and - obj.getLock() == false and - obj.getDescription() ~= "Action Token" then + obj.getLock() == false then ownedObjects.Trash.putObject(obj) end end @@ -1671,77 +1709,147 @@ end -- investigator ID grabbing and skill tracker --------------------------------------------------------- +-- updates the internal investigator id and action tokens if an investigator card is detected +---@param card tts__Object Card that might be an investigator function maybeUpdateActiveInvestigator(card) if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end local notes = JSON.decode(card.getGMNotes()) - local class + local extraToken if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then if notes.id == activeInvestigatorId then return end - class = notes.class + activeInvestigatorClass = notes.class activeInvestigatorId = notes.id + extraToken = notes.extraToken ownedObjects.InvestigatorSkillTracker.call("updateStats", { notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons }) + updateTexture() elseif activeInvestigatorId ~= "00000" then - class = "Neutral" + activeInvestigatorClass = "Neutral" activeInvestigatorId = "00000" - ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1}) + ownedObjects.InvestigatorSkillTracker.call("updateStats", { 1, 1, 1, 1 }) + updateTexture() else return end - -- change state of action tokens - local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1}) - local smallToken = nil - local STATE_TABLE = { - ["Guardian"] = 1, - ["Seeker"] = 2, - ["Rogue"] = 3, - ["Mystic"] = 4, - ["Survivor"] = 5, - ["Neutral"] = 6 - } + -- set proper scale for investigators + local cardData = card.getData() + if cardData["SidewaysCard"] == true then + -- 115% for easier readability + card.setScale({ 1.15, 1, 1.15 }) + else + -- Zoop-exported investigators are horizontal cards and TTS scales them differently + card.setScale({ 0.8214, 1, 0.8214 }) + end - for _, obj in ipairs(search) do - if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then - if obj.getScale().x < 0.4 then - smallToken = obj + -- remove old action tokens + for _, obj in ipairs(searchAroundSelf("isUniversalToken")) do + obj.destruct() + end + + -- spawn three regular action tokens (investigator specific one in the bottom spot) + for i = 1, 3 do + local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(pos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = activeInvestigatorClass }) + end) + end + + -- spawn additional token (maybe specific for investigator) + if extraToken and extraToken ~= "None" then + -- local positions + local tokenSpawnPos = { + action = { + Vector(-0.86, 0, -0.28), -- left of the regular three actions + Vector(-1.54, 0, -0.28), -- right of the regular three actions + }, + ability = { + Vector(-1, 0, 0.118), -- bottom left corner of the investigator card + Vector(-1, 0, -0.118), -- top left corner of the investigator card + } + } + + -- spawn tokens (split string by "|") + local count = { action = 0, ability = 0 } + for str in string.gmatch(extraToken, "([^|]+)") do + local type = "action" + if str == "FreeTrigger" or str == "Reaction" then + type = "ability" + end + + count[type] = count[type] + 1 + if count[type] > 2 then + printToColor("More than two extra tokens of the same type are not supported.", playerColor) else - setObjectState(obj, STATE_TABLE[class]) + local localSpawnPos = tokenSpawnPos[type][count[type]] + local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = str }) + end) end end end - - -- update the small token with special action for certain investigators - local SPECIAL_ACTIONS = { - ["04002"] = 8, -- Ursula Downs - ["01002"] = 9, -- Daisy Walker - ["01502"] = 9, -- Daisy Walker - ["01002-pb"] = 9, -- Daisy Walker - ["06003"] = 10, -- Tony Morgan - ["04003"] = 11, -- Finn Edwards - ["08016"] = 14 -- Bob Jenkins - } - - if smallToken ~= nil then - setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class]) - end end -function setObjectState(obj, stateId) - if obj.getStateId() ~= stateId then obj.setState(stateId) end +-- updates the texture of the playermat +---@param overrideName? string Force a specific texture +function updateTexture(overrideName) + local name = "Neutral" + + -- use class specific texture if enabled + if isClassTextureEnabled then + name = activeInvestigatorClass + end + + -- get new texture URL + local newUrl = nameToTexture[name] + + -- override name if valid + if nameToTexture[overrideName] then + newUrl = nameToTexture[overrideName] + end + + -- apply texture + local customInfo = self.getCustomObject() + if customInfo.image ~= newUrl then + -- temporarily lock objects so they don't fall through the mat + local objectsToUnlock = {} + for _, obj in ipairs(searchAroundSelf()) do + if not obj.getLock() then + obj.setLock(true) + table.insert(objectsToUnlock, obj) + end + end + + self.script_state = onSave() + customInfo.image = newUrl + self.setCustomObject(customInfo) + local reloadedMat = self.reload() + + -- unlock objects when mat is reloaded + Wait.condition(function() + for _, obj in ipairs(objectsToUnlock) do + obj.setLock(false) + end + end, function() return reloadedMat.loading_custom == false end) + end end --------------------------------------------------------- -- manipulation of owned objects --------------------------------------------------------- --- updates the specific owned counter +-- updates the specified owned counter ---@param param table Contains the information to update: --- type: String Counter to target --- newValue: Number Value to set the counter to @@ -1755,20 +1863,21 @@ function updateCounter(param) end end --- returns the resource counter amount +-- get the value the specified owned counter ---@param type string Counter to target +---@return number: Counter value function getCounterValue(type) return ownedObjects[type].getVar("val") end -- set investigator skill tracker to "1, 1, 1, 1" function resetSkillTracker() - local obj = ownedObjects.InvestigatorSkillTracker - if obj ~= nil then - obj.call("updateStats", { 1, 1, 1, 1 }) - else - printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow") - end + local obj = ownedObjects.InvestigatorSkillTracker + if obj ~= nil then + obj.call("updateStats", { 1, 1, 1, 1 }) + else + printToAll("Skill tracker for " .. matColor .. " playermat could not be found.", "Yellow") + end end --------------------------------------------------------- @@ -1780,32 +1889,53 @@ function drawChaosTokenButton(_, _, isRightClick) end function drawEncounterCard(_, _, isRightClick) - mythosAreaApi.drawEncounterCard(self, isRightClick) + local drawPos = getEncounterCardDrawPosition(not isRightClick) + mythosAreaApi.drawEncounterCard(matColor, drawPos) end function returnGlobalDiscardPosition() return self.positionToWorld(DISCARD_PILE_POSITION) end --- Sets this playermat's draw 1 button to visible +function returnGlobalDrawPosition() + return self.positionToWorld(DRAW_DECK_POSITION) +end + +-- returns the position for encounter card drawing +---@param stack boolean If true, returns the leftmost position instead of the first empty from the right +function getEncounterCardDrawPosition(stack) + local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION) + + -- maybe override position with first empty slot in threat area (right to left) + if not stack then + local searchPos = Vector(-0.91, 0.5, -0.625) + for i = 1, 5 do + local globalSearchPos = self.positionToWorld(searchPos) + local searchResult = searchLib.atPosition(globalSearchPos, "isCardOrDeck") + if #searchResult == 0 then + drawPos = globalSearchPos + break + else + searchPos.x = searchPos.x + 0.455 + end + end + end + + return drawPos +end + +-- creates / removes the draw 1 button ---@param visible boolean Whether the draw 1 button should be visible function showDrawButton(visible) isDrawButtonVisible = visible - -- create the "Draw 1" button if isDrawButtonVisible then - self.createButton({ - label = "Draw 1", - click_function = "doDrawOne", - function_owner = self, - position = { 1.84, 0.1, -0.36 }, - scale = { 0.12, 0.12, 0.12 }, - width = 800, - height = 280, - font_size = 180 - }) - - -- remove the "Draw 1" button + -- Draw 1 button: modified default data + buttonParameters.label = "Draw 1" + buttonParameters.click_function = "doDrawOne" + buttonParameters.tooltip = "" + buttonParameters.position.z = -0.35 + self.createButton(buttonParameters) else local buttons = self.getButtons() for i = 1, #buttons do @@ -1816,18 +1946,18 @@ function showDrawButton(visible) end end --- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues +-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues ---@param showCounter boolean Whether the clickable clue counter should be visible function clickableClues(showCounter) local clickerPos = ownedObjects.ClickableClueCounter.getPosition() local clueCount = 0 - + -- move clue counters local modY = showCounter and 0.525 or -0.525 ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) if showCounter then - -- current clue count + -- get current clue count clueCount = ownedObjects.ClueCounter.getVar("exposedValue") -- remove clues @@ -1836,11 +1966,11 @@ function clickableClues(showCounter) -- set value for clue clickers ownedObjects.ClickableClueCounter.call("updateVal", clueCount) else - -- current clue count + -- get current clue count clueCount = ownedObjects.ClickableClueCounter.getVar("val") -- spawn clues - local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7}) + local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 }) for i = 1, clueCount do pos.y = pos.y + 0.045 * i tokenManager.spawnToken(pos, "clue", self.getRotation()) @@ -1848,6 +1978,14 @@ function clickableClues(showCounter) end end +-- Toggles the use of class textures +---@param state boolean Whether the class texture should be used or not +function useClassTexture(state) + if state == isClassTextureEnabled then return end + isClassTextureEnabled = state + updateTexture() +end + -- removes all clues (moving tokens to the trash and setting counters to 0) function removeClues() ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) @@ -1871,8 +2009,7 @@ end function setLimitSnapsByType(matchTypes) local snaps = self.getSnapPoints() for i, snap in ipairs(snaps) do - local snapPos = snap.position - if inArea(snapPos, MAIN_PLAY_AREA) then + if inArea(snap.position, MAIN_PLAY_AREA) then local snapTags = snaps[i].tags if matchTypes then if snapTags == nil then @@ -1884,7 +2021,7 @@ function setLimitSnapsByType(matchTypes) snaps[i].tags = nil end end - if inArea(snapPos, INVESTIGATOR_AREA) then + if inArea(snap.position, INVESTIGATOR_AREA) then local snapTags = snaps[i].tags if matchTypes then if snapTags == nil then @@ -1900,16 +2037,15 @@ function setLimitSnapsByType(matchTypes) self.setSnapPoints(snaps) end --- 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 tts__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 --- bounds definition. +---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition. ---@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 -- called by custom data helpers to add player card data @@ -1919,125 +2055,752 @@ function updatePlayerCards(args) local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") tokenManager.addPlayerCardData(playerCardData) end + +-- returns the colored steam name or color +function getColoredName(playerColor) + local displayName = playerColor + if Player[playerColor].steam_name then + displayName = Player[playerColor].steam_name + end + + -- add bb-code + return "[" .. Color.fromString(playerColor):toHex() .. "]" .. displayName .. "[-]" +end +end) +__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local ChaosBagApi = {} + + -- respawns the chaos bag with a new state of tokens + ---@param tokenList table List of chaos token ids + ChaosBagApi.setChaosBagState = function(tokenList) + Global.call("setChaosBagState", tokenList) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getChaosBagState = function() + local chaosBagContentsCatcher = Global.call("getChaosBagState") + local chaosBagContents = {} + for _, v in ipairs(chaosBagContentsCatcher) do + table.insert(chaosBagContents, v) + end + return chaosBagContents + end + + -- checks scripting zone for chaos bag (also called by a lot of objects!) + ChaosBagApi.findChaosBag = function() + return Global.call("findChaosBag") + end + + -- returns a table of object references to the tokens in play (does not include sealed tokens!) + ChaosBagApi.getTokensInPlay = function() + return Global.call("getChaosTokensinPlay") + end + + -- returns all sealed tokens on cards to the chaos bag + ---@param playerColor string Color of the player to show the broadcast to + ChaosBagApi.releaseAllSealedTokens = function(playerColor) + Global.call("releaseAllSealedTokens", playerColor) + end + + -- returns all drawn tokens to the chaos bag + ChaosBagApi.returnChaosTokens = function() + Global.call("returnChaosTokens") + end + + -- removes the specified chaos token from the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.removeChaosToken = function(id) + Global.call("removeChaosToken", id) + end + + -- returns a chaos token to the bag and calls all relevant functions + ---@param token tts__Object Chaos token to return + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) + end + + -- spawns the specified chaos token and puts it into the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.spawnChaosToken = function(id) + Global.call("spawnChaosToken", id) + end + + -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens + -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the + -- contents of the bag should check this method before doing so. + -- This method will broadcast a message to all players if the bag is being searched. + ---@return any: True if the bag is manipulated, false if it should be blocked. + ChaosBagApi.canTouchChaosTokens = function() + return Global.call("canTouchChaosTokens") + end + + -- draws a chaos token to a playermat + ---@param mat tts__Object Playermat that triggered this + ---@param drawAdditional boolean Controls whether additional tokens should be drawn + ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag + ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getIdUrlMap = function() + return Global.getTable("ID_URL_MAP") + end + + return ChaosBagApi +end +end) +__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local CHAOS_TOKEN_NAMES = { + ["Elder Sign"] = true, + ["+1"] = true, + ["0"] = true, + ["-1"] = true, + ["-2"] = true, + ["-3"] = true, + ["-4"] = true, + ["-5"] = true, + ["-6"] = true, + ["-7"] = true, + ["-8"] = true, + ["Skull"] = true, + ["Cultist"] = true, + ["Tablet"] = true, + ["Elder Thing"] = true, + ["Auto-fail"] = true, + ["Bless"] = true, + ["Curse"] = true, + ["Frost"] = true + } + + local TokenChecker = {} + + -- returns true if the passed object is a chaos token (by name) + TokenChecker.isChaosToken = function(obj) + if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then + return true + else + return false + end + end + + return TokenChecker +end +end) +__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local TokenSpawnTracker = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getSpawnTracker() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenSpawnTracker") + end + + TokenSpawnTracker.hasSpawnedTokens = function(cardGuid) + return getSpawnTracker().call("hasSpawnedTokens", cardGuid) + end + + TokenSpawnTracker.markTokensSpawned = function(cardGuid) + return getSpawnTracker().call("markTokensSpawned", cardGuid) + end + + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) + end + + TokenSpawnTracker.resetAllAssetAndEvents = function() + return getSpawnTracker().call("resetAllAssetAndEvents") + end + + TokenSpawnTracker.resetAllLocations = function() + return getSpawnTracker().call("resetAllLocations") + end + + TokenSpawnTracker.resetAll = function() + return getSpawnTracker().call("resetAll") + end + + return TokenSpawnTracker +end end) __bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local DeckLib = {} local searchLib = require("util/SearchLib") - -- places a card/deck at a position or merges into an existing deck - ---@param obj tts__Object Object to move + -- places a card/deck at a position or merges into an existing deck below + ---@param objOrTable tts__Object|table Object or table of objects to move ---@param pos table New position for the object - ---@param rot table New rotation for the object (optional) - DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot) - if obj == nil or pos == nil then return end + ---@param rot? table New rotation for the object + ---@param below? boolean Should the object be placed below an existing deck? + DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) + if objOrTable == nil or pos == nil then return end + + -- handle 'objOrTable' parameter + local objects = {} + if type(objOrTable) == "table" then + objects = objOrTable + else + table.insert(objects, objOrTable) + end -- search the new position for existing card/deck local searchResult = searchLib.atPosition(pos, "isCardOrDeck") + local targetObj -- get new position - local newPos local offset = 0.5 + local newPos = Vector(pos) + Vector(0, offset, 0) + if #searchResult == 1 then - local bounds = searchResult[1].getBounds() - newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) - else - newPos = Vector(pos) + Vector(0, offset, 0) - end - - -- allow moving the objects smoothly out of the hand - obj.use_hands = false - - if rot then - obj.setRotationSmooth(rot, false, true) - end - obj.setPositionSmooth(newPos, false, true) - - -- continue if the card stops smooth moving - Wait.condition( - function() - obj.use_hands = true - -- this avoids a TTS bug that merges unrelated cards that are not resting - 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) + targetObj = searchResult[1] + local bounds = targetObj.getBounds() + if below then + newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) + else + newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) end - end, - function() return not obj.isSmoothMoving() end, 3) + end + + -- process objects in reverse order + for i = #objects, 1, -1 do + local obj = objects[i] + -- add a 0.1 delay for each object (for animation purposes) + Wait.time(function() + -- allow moving smoothly out of hand and temporarily lock it + obj.setLock(true) + obj.use_hands = false + + if rot then + obj.setRotationSmooth(rot, false, true) + end + obj.setPositionSmooth(newPos, false, true) + + -- wait for object to finish movement (or 2 seconds) + Wait.condition( + function() + -- revert toggles + obj.setLock(false) + obj.use_hands = true + + -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting + if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then + targetObj = targetObj.putObject(obj) + else + targetObj = obj + end + end, + -- check state of the object (make sure it's not moving) + function() return obj.isDestroyed() or not obj.isSmoothMoving() end, + 2) + end, (#objects- i) * 0.1) + end end return DeckLib end end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + end + + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + end + + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") + end + + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) + end + + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) + end + + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) + end + + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) + end + + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) + return t + end - -- filtering the result + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi +end +end) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") + else + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } + end + end + + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end + end + return result + end + + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) end end return objList end - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end end - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end end - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib + return PlayermatApi end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("playermat/Playermat") +end) return __bundle_require("__root") diff --git a/unpacked/Custom_Tile Playermat 3 Green 383d8b.yaml b/unpacked/Custom_Tile Playermat 3 Green 383d8b.yaml index 93aeebd67..4335423aa 100644 --- a/unpacked/Custom_Tile Playermat 3 Green 383d8b.yaml +++ b/unpacked/Custom_Tile Playermat 3 Green 383d8b.yaml @@ -8,61 +8,61 @@ AttachedSnapPoints: y: 0.1 z: 0.12 Tags: - - ActionToken + - UniversalToken - Position: x: -0.86 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1 + x: -1.03 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.18 + x: -1.2 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.36 + x: -1.37 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -0.63 + x: -1.54 y: 0.1 - z: 0.55 + z: -0.28 + Tags: + - UniversalToken +- Position: + x: 1.76 + y: 0.1 + z: 0.04 Tags: - Asset - Position: - x: -0.62 + x: 1.37 y: 0.1 - z: 0.02 + z: 0.04 Tags: - Asset - Position: - x: -0.18 + x: 0.98 + y: 0.1 + z: 0.04 + Tags: + - Asset +- Position: + x: 0.6 y: 0.1 z: 0.03 Tags: - Asset -- Position: - x: -0.17 - y: 0.1 - z: 0.55 - Tags: - - Asset -- Position: - x: 0.21 - y: 0.1 - z: 0.56 - Tags: - - Asset - Position: x: 0.22 y: 0.1 @@ -70,39 +70,15 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 0.6 + x: -0.18 y: 0.1 z: 0.03 Tags: - Asset - Position: - x: 0.6 + x: -0.62 y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.56 + z: 0.02 Tags: - Asset - Position: @@ -112,9 +88,39 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 1.76 + x: 1.37 y: 0.1 - z: 0.04 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.98 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.6 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.21 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: -0.17 + y: 0.1 + z: 0.55 + Tags: + - Asset +- Position: + x: -0.63 + y: 0.1 + z: 0.55 Tags: - Asset - Position: @@ -208,7 +214,7 @@ CustomImage: Type: 3 ImageScalar: 1 ImageSecondaryURL: '' - ImageURL: http://cloud-3.steamusercontent.com/ugc/2037357630681963618/E7271737B19CE0BFAAA382BEEEF497FE3E06ECC1/ + ImageURL: http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/ WidthScale: 0 Description: '' DragSelectable: true @@ -222,7 +228,8 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Tile Playermat 3 Green 383d8b.ttslua' -LuaScriptState: '{"activeInvestigatorId":"00000","isDrawButtonVisible":false,"playerColor":"Green"}' +LuaScriptState: '{"activeInvestigatorClass":"Neutral","activeInvestigatorId":"00000","isClassTextureEnabled":true,"isDrawButtonVisible":false,"playerColor":"Green","slotData":["any","any","any","Tarot","Hand + (left)","Hand (right)","Ally","any","any","any","Accessory","Arcane","Arcane","Body"]}' MeasureMovement: false Memo: Green Name: Custom_Tile diff --git a/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua b/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua index 9f92b3fcd..a1780a348 100644 --- a/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua +++ b/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua @@ -41,174 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("playermat/Playmat") -end) -__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local ChaosBagApi = {} - - -- respawns the chaos bag with a new state of tokens - ---@param tokenList table List of chaos token ids - ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getChaosBagState = function() - local chaosBagContentsCatcher = Global.call("getChaosBagState") - local chaosBagContents = {} - for _, v in ipairs(chaosBagContentsCatcher) do - table.insert(chaosBagContents, v) - end - return chaosBagContents - end - - -- checks scripting zone for chaos bag (also called by a lot of objects!) - ChaosBagApi.findChaosBag = function() - return Global.call("findChaosBag") - end - - -- returns a table of object references to the tokens in play (does not include sealed tokens!) - ChaosBagApi.getTokensInPlay = function() - return Global.call("getChaosTokensinPlay") - end - - -- returns all sealed tokens on cards to the chaos bag - ---@param playerColor string Color of the player to show the broadcast to - ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) - end - - -- returns all drawn tokens to the chaos bag - ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") - end - - -- removes the specified chaos token from the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) - end - - -- returns a chaos token to the bag and calls all relevant functions - ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) - end - - -- spawns the specified chaos token and puts it into the chaos bag - ---@param id string ID of the chaos token - ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) - end - - -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens - -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the - -- contents of the bag should check this method before doing so. - -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. - ChaosBagApi.canTouchChaosTokens = function() - return Global.call("canTouchChaosTokens") - end - - -- called by playermats (by the "Draw chaos token" button) - ---@param mat tts__Object Playermat that triggered this - ---@param drawAdditional boolean Controls whether additional tokens should be drawn - ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag - ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) - end - - -- returns a Table List of chaos token ids in the current chaos bag - -- requires copying the data into a new table because TTS is weird about handling table return values in Global - ChaosBagApi.getIdUrlMap = function() - return Global.getTable("ID_URL_MAP") - end - - return ChaosBagApi -end -end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) -__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local MythosAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getMythosArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") - end - - ---@return any: Table of chaos token metadata (if provided through scenario reference card) - MythosAreaApi.returnTokenData = function() - return getMythosArea().call("returnTokenData") - end - - ---@return any: Object reference to the encounter deck - MythosAreaApi.getEncounterDeck = function() - return getMythosArea().call("getEncounterDeck") - end - - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) - end - - -- reshuffle the encounter deck - MythosAreaApi.reshuffleEncounterDeck = function() - getMythosArea().call("reshuffleEncounterDeck") - end - - return MythosAreaApi -end -end) __bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local NavigationOverlayApi = {} @@ -247,184 +79,12 @@ do return NavigationOverlayApi end end) -__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local OptionPanelApi = {} - - -- loads saved options - ---@param options table Set a new state for the option table - OptionPanelApi.loadSettings = function(options) - return Global.call("loadSettings", options) - end - - ---@return any: Table of option panel state - OptionPanelApi.getOptions = function() - return Global.getTable("optionPanel") - end - - return OptionPanelApi -end -end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local PlayAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") - end - - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") - end - - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") - end - - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) - end - - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) - end - - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) - end - - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) - end - - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) - end - - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) - end - - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) - end - - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) - end - - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) - end - - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi -end -end) -__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local CHAOS_TOKEN_NAMES = { - ["Elder Sign"] = true, - ["+1"] = true, - ["0"] = true, - ["-1"] = true, - ["-2"] = true, - ["-3"] = true, - ["-4"] = true, - ["-5"] = true, - ["-6"] = true, - ["-7"] = true, - ["-8"] = true, - ["Skull"] = true, - ["Cultist"] = true, - ["Tablet"] = true, - ["Elder Thing"] = true, - ["Auto-fail"] = true, - ["Bless"] = true, - ["Curse"] = true, - ["Frost"] = true - } - - local TokenChecker = {} - - -- returns true if the passed object is a chaos token (by name) - TokenChecker.isChaosToken = function(obj) - if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then - return true - else - return false - end - end - - return TokenChecker -end -end) __bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules) do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -555,13 +215,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -576,11 +236,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -595,18 +255,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -618,11 +281,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -632,7 +294,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -658,16 +324,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -677,9 +343,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -715,21 +380,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -768,7 +425,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -778,11 +435,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -801,7 +458,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -818,7 +475,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -829,7 +486,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -902,21 +559,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -965,8 +617,8 @@ do return getSpawnTracker().call("markTokensSpawned", cardGuid) end - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) end TokenSpawnTracker.resetAllAssetAndEvents = function() @@ -984,120 +636,175 @@ do return TokenSpawnTracker end end) -__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules) -local chaosBagApi = require("chaosbag/ChaosBagApi") -local deckLib = require("util/DeckLib") -local guidReferenceApi = require("core/GUIDReferenceApi") -local mythosAreaApi = require("core/MythosAreaApi") -local navigationOverlayApi = require("core/NavigationOverlayApi") -local searchLib = require("util/SearchLib") -local tokenChecker = require("core/token/TokenChecker") -local tokenManager = require("core/token/TokenManager") +__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local OptionPanelApi = {} + + -- loads saved options + ---@param options table Set a new state for the option table + OptionPanelApi.loadSettings = function(options) + return Global.call("loadSettings", options) + end + + ---@return any: Table of option panel state + OptionPanelApi.getOptions = function() + return Global.getTable("optionPanel") + end + + return OptionPanelApi +end +end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("playermat/Playermat") +end) +__bundle_register("playermat/Playermat", function(require, _LOADED, __bundle_register, __bundle_modules) +local chaosBagApi = require("chaosbag/ChaosBagApi") +local deckLib = require("util/DeckLib") +local guidReferenceApi = require("core/GUIDReferenceApi") +local mythosAreaApi = require("core/MythosAreaApi") +local navigationOverlayApi = require("core/NavigationOverlayApi") +local searchLib = require("util/SearchLib") +local tokenChecker = require("core/token/TokenChecker") +local tokenManager = require("core/token/TokenManager") +local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") -- we use this to turn off collision handling until onLoad() is complete -local collisionEnabled = false +local collisionEnabled = false +local currentlyEditingSlots = false -- x-Values for discard buttons -local DISCARD_BUTTON_OFFSETS = {-1.365, -0.91, -0.455, 0, 0.455, 0.91} +local DISCARD_BUTTON_X_START = -1.365 +local DISCARD_BUTTON_X_OFFSET = 0.455 local SEARCH_AROUND_SELF_X_BUFFER = 8 +local SEARCH_AROUND_SELF_Z_BUFFER = 1.75 -- defined areas for object searching -local MAIN_PLAY_AREA = { - upperLeft = { - x = 1.98, - z = 0.736 - }, - lowerRight = { - x = -0.79, - z = -0.39 - } +local MAIN_PLAY_AREA = { + upperLeft = { x = 1.98, z = 0.736 }, + lowerRight = { x = -0.79, z = -0.39 } } -local INVESTIGATOR_AREA = { - upperLeft = { - x = -1.084, - z = 0.06517 - }, - lowerRight = { - x = -1.258, - z = -0.0805 - } +local INVESTIGATOR_AREA = { + upperLeft = { x = -1.084, z = 0.06517 }, + lowerRight = { x = -1.258, z = -0.0805 } } -local THREAT_AREA = { - upperLeft = { - x = 1.53, - z = -0.34 - }, - lowerRight = { - x = -1.13, - z = -0.92 - } +local THREAT_AREA = { + upperLeft = { x = 1.53, z = -0.34 }, + lowerRight = { x = -1.13, z = -0.92 } } -local DECK_DISCARD_AREA = { - upperLeft = { - x = -1.62, - z = 0.855 - }, - lowerRight = { - x = -2.02, - z = -0.245 - }, - center = { - x = -1.82, - y = 0.5, - z = 0.305 - }, - size = { - x = 0.4, - y = 3, - z = 1.1 - } +local DECK_DISCARD_AREA = { + upperLeft = { x = -1.62, z = 0.855 }, + lowerRight = { x = -2.02, z = -0.245 }, + center = { x = -1.82, y = 0.5, z = 0.305 }, + size = { x = 0.4, y = 3, z = 1.1 } } --- local position of draw and discard pile -local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } -local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +-- local positions +local DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 } +local DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 } +local DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 } -- global position of encounter discard pile -local ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38} +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" +-- used for the buttons on the right side of the playermat +-- starts off with the data for the "Upkeep" button and will then be changed +local buttonParameters = { + label = "Upkeep", + click_function = "doUpkeep", + tooltip = "Right-click to change color", + function_owner = self, + position = { x = 1.82, y = 0.1, z = -0.45 }, + scale = { 0.12, 0.12, 0.12 }, + width = 1000, + height = 280, + font_size = 180 +} + +-- table of texture URLs +local nameToTexture = { + Guardian = "http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/", + Mystic = "http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/", + Neutral = "http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/", + Rogue = "http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/", + Seeker = "http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/", + Survivor = "http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/" +} + +-- translation table for slot names to characters for special font +local slotNameToChar = { + ["any"] = "", + ["Accessory"] = "C", + ["Ally"] = "E", + ["Arcane"] = "G", + ["Body"] = "K", + ["Hand (right)"] = "M", + ["Hand (left)"] = "M", + ["Hand x2"] = "N", + ["Tarot"] = "A" +} + +-- slot symbol for the respective slot (from top left to bottom right) - intentionally global! +slotData = {} +local defaultSlotData = { + -- 1st row + "any", "any", "any", "Tarot", "Hand (left)", "Hand (right)", "Ally", + + -- 2nd row + "any", "any", "any", "Accessory", "Arcane", "Arcane", "Body" +} + +-- global variables for access +activeInvestigatorClass = "Neutral" +activeInvestigatorId = "00000" +hasDES = false + +local isClassTextureEnabled = true +local isDrawButtonVisible = false -- table of type-object reference pairs of all owned objects -local ownedObjects = {} -local matColor = self.getMemo() - --- variable to track the status of the "Show Draw Button" option -local isDrawButtonVisible = false - --- global variable to report "Dream-Enhancing Serum" status -isDES = false +local ownedObjects = {} +local matColor = self.getMemo() function onSave() return JSON.encode({ - playerColor = playerColor, + activeInvestigatorClass = activeInvestigatorClass, activeInvestigatorId = activeInvestigatorId, - isDrawButtonVisible = isDrawButtonVisible + isClassTextureEnabled = isClassTextureEnabled, + isDrawButtonVisible = isDrawButtonVisible, + playerColor = playerColor, + slotData = slotData }) end -function onLoad(saveState) +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) + activeInvestigatorClass = loadedData.activeInvestigatorClass + activeInvestigatorId = loadedData.activeInvestigatorId + isClassTextureEnabled = loadedData.isClassTextureEnabled + isDrawButtonVisible = loadedData.isDrawButtonVisible + playerColor = loadedData.playerColor + slotData = loadedData.slotData + end + + updateMessageColor(playerColor) + self.interactable = false -- get object references to owned objects ownedObjects = guidReferenceApi.getObjectsByOwner(matColor) - -- button creation + -- discard button creation for i = 1, 6 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], i) + makeDiscardButton(i) end self.createButton({ click_function = "drawEncounterCard", function_owner = self, - position = {-1.84, 0, -0.65}, - rotation = {0, 80, 0}, + position = { -1.84, 0, -0.65 }, + rotation = { 0, 80, 0 }, width = 265, height = 190 }) @@ -1105,32 +812,24 @@ function onLoad(saveState) self.createButton({ click_function = "drawChaosTokenButton", function_owner = self, - position = {1.85, 0, -0.74}, - rotation = {0, -45, 0}, + position = { 1.85, 0, -0.74 }, + rotation = { 0, -45, 0 }, width = 135, height = 135 }) - self.createButton({ - label = "Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.84, 0.1, -0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) + -- Upkeep button: can use the default parameters for this + self.createButton(buttonParameters) - -- save state loading - local state = JSON.decode(saveState) - if state ~= nil then - playerColor = state.playerColor - activeInvestigatorId = state.activeInvestigatorId - isDrawButtonVisible = state.isDrawButtonVisible - end + -- Slot editing button: modified default data + buttonParameters.label = "Edit Slots" + buttonParameters.click_function = "toggleSlotEditing" + buttonParameters.tooltip = "Right-click to reset slot symbols" + buttonParameters.position.z = 0.92 + self.createButton(buttonParameters) showDrawButton(isDrawButtonVisible) + redrawSlotSymbols() math.randomseed(os.time()) Wait.time(function() collisionEnabled = true end, 0.1) end @@ -1144,19 +843,25 @@ function searchArea(origin, size, filter) return searchLib.inArea(origin, self.getRotation(), size, filter) end --- finds all objects on the playmat and associated set aside zone. +-- finds all objects on the playermat and associated set aside zone. function searchAroundSelf(filter) + local scale = self.getScale() local bounds = self.getBoundsNormalized() + -- Increase the width to cover the set aside zone bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER bounds.size.y = 1 - -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge - -- of the cast at the edge of the playmat - -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the - -- table position of the playmat + bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER + + -- 'setAsideDirection' accounts for the set aside zone being on the left or right, + -- depending on the table position of the playermat local setAsideDirection = bounds.center.z > 0 and 1 or -1 + + -- Since the cast is centered on the position, shift left or right to keep + -- the non-set aside edge of the cast at the edge of the playermat 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 / scale.x + localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z return searchArea(self.positionToWorld(localCenter), bounds.size, filter) end @@ -1166,24 +871,39 @@ function searchDeckAndDiscardArea(filter) local scale = self.getScale() local size = { x = DECK_DISCARD_AREA.size.x * scale.x, - y = DECK_DISCARD_AREA.size.y, + y = DECK_DISCARD_AREA.size.y, z = DECK_DISCARD_AREA.size.z * scale.z } return searchArea(pos, size, filter) end -function doNotReady(card) - return card.getVar("do_not_ready") or false -end - -- rounds a number to the specified amount of decimal places ---@param num number Initial value ---@param numDecimalPlaces number Amount of decimal places +---@return number: rounded number function round(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) + local mult = 10 ^ (numDecimalPlaces or 0) return math.floor(num * mult + 0.5) / mult end +-- edits the label of a button +---@param oldLabel string Old label of the button +---@param newLabel string New label of the button +function editButtonLabel(oldLabel, newLabel) + local buttons = self.getButtons() + for i = 1, #buttons do + if buttons[i].label == oldLabel then + self.editButton({ index = buttons[i].index, label = newLabel }) + end + end +end + +-- updates the internal "messageColor" which is used for print/broadcast statements if no player is seated +---@param clickedByColor string Colorstring of player who clicked a button +function updateMessageColor(clickedByColor) + messageColor = Player[playerColor].seated and playerColor or clickedByColor +end + --------------------------------------------------------- -- Discard buttons --------------------------------------------------------- @@ -1196,25 +916,27 @@ function discardListOfObjects(objList) if obj.hasTag("PlayerCard") then deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation()) else - deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, {x = 0, y = -90, z = 0}) + deckLib.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 - chaosBagApi.returnChaosTokenToBag(obj) - -- don't touch locked objects (like the table etc.) - elseif not obj.getLock() then + -- put chaos tokens back into bag (e.g. Unrelenting) + chaosBagApi.returnChaosTokenToBag(obj, false) + elseif not obj.getLock() and not obj.hasTag("DontDiscard") then + -- don't touch locked objects (like the table etc.) or specific objects (like key tokens) ownedObjects.Trash.putObject(obj) end 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 handlerName = 'handler' .. number +-- build a discard button to discard from searchPosition +---@param id number Index of the discard button (from left to right, must be unique) +function makeDiscardButton(id) + local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET + local position = { xValue, 0.1, -0.94 } + local searchPosition = { -position[1], position[2], position[3] + 0.32 } + local handlerName = 'handler' .. id self.setVar(handlerName, function() - local cardSizeSearch = {2, 1, 3.2} + local cardSizeSearch = { 2, 1, 3.2 } local globalSearchPosition = self.positionToWorld(searchPosition) local searchResult = searchArea(globalSearchPosition, cardSizeSearch) return discardListOfObjects(searchResult) @@ -1224,7 +946,7 @@ function makeDiscardButton(xValue, number) click_function = handlerName, function_owner = self, position = {position[1], position[2], position[3] + 0.6}, - scale = {0.12, 0.12, 0.12}, + scale = { 0.12, 0.12, 0.12 }, width = 900, height = 350, font_size = 220 @@ -1236,8 +958,8 @@ end --------------------------------------------------------- -- calls the Upkeep function with correct parameter -function doUpkeepFromHotkey(color) - doUpkeep(_, color) +function doUpkeepFromHotkey(clickedByColor) + doUpkeep(_, clickedByColor) end function doUpkeep(_, clickedByColor, isRightClick) @@ -1246,18 +968,20 @@ function doUpkeep(_, clickedByColor, isRightClick) return end - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or clickedByColor + updateMessageColor(clickedByColor) - -- unexhaust cards in play zone, flip action tokens and find forcedLearning + -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum + checkForDES() local forcedLearning = false local rot = self.getRotation() for _, obj in ipairs(searchAroundSelf()) do - if obj.getDescription() == "Action Token" and obj.is_face_down then + if obj.hasTag("Temporary") == true then + discardListOfObjects({ obj }) + elseif obj.hasTag("UniversalToken") == true and obj.is_face_down then obj.flip() elseif obj.type == "Card" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then local cardMetadata = JSON.decode(obj.getGMNotes()) or {} - if not doNotReady(obj) then + if not (obj.getVar("do_not_ready") or obj.hasTag("DoNotReady")) then local cardRotation = round(obj.getRotation().y, 0) - rot.y local yRotDiff = 0 @@ -1274,14 +998,26 @@ function doUpkeep(_, clickedByColor, isRightClick) -- set correct rotation for face-down cards rot.z = obj.is_face_down and 180 or 0 - obj.setRotation({rot.x, rot.y + yRotDiff, rot.z}) + obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z }) end + + -- detect Forced Learning to handle card drawing accordingly if cardMetadata.id == "08031" then forcedLearning = true end - if cardMetadata.uses ~= nil then + + -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile) + if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x > -1 then tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self) end + elseif obj.type == "Deck" and forcedLearning == false then + -- check decks for forced learning + for _, deepObj in ipairs(obj.getObjects()) do + local cardMetadata = JSON.decode(deepObj.gm_notes) or {} + if cardMetadata.id == "08031" then + forcedLearning = true + end + end end end @@ -1299,22 +1035,44 @@ function doUpkeep(_, clickedByColor, isRightClick) -- gain a resource (or two if playing Jenny Barnes) if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then - updateCounter({type = "ResourceCounter", modifier = 2}) + updateCounter({ type = "ResourceCounter", modifier = 2 }) printToColor("Gaining 2 resources (Jenny)", messageColor) else - updateCounter({type = "ResourceCounter", modifier = 1}) + updateCounter({ type = "ResourceCounter", modifier = 1 }) end -- draw a card (with handling for Patrice and Forced Learning) if activeInvestigatorId == "06005" then if forcedLearning then - printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) + printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?" + .. " Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor) else - local handSize = #Player[playerColor].getHandObjects() - if handSize < 5 then - local cardsToDraw = 5 - handSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - drawCardsWithReshuffle(cardsToDraw) + -- discards all non-weakness and non-hidden cards from hand first + local handCards = Player[playerColor].getHandObjects() + local cardsToDiscard = {} + + for _, card in ipairs(handCards) do + local md = JSON.decode(card.getGMNotes()) + if card.type == "Card" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= "52020") then + table.insert(cardsToDiscard, card) + end + end + + -- perform discarding 1 by 1 + local pos = returnGlobalDiscardPosition() + deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation()) + + -- draw up to 5 cards + local cardsToDraw = 5 - #handCards + #cardsToDiscard + if cardsToDraw > 0 then + printToColor("Discarding " .. #cardsToDiscard .. " and drawing " .. cardsToDraw .. " card(s). (Patrice)", messageColor) + + -- add some time if there are any cards to discard + local k = 0 + if #cardsToDiscard > 0 then + k = 0.8 + (#cardsToDiscard * 0.1) + end + Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k) end end elseif forcedLearning then @@ -1328,14 +1086,14 @@ function doUpkeep(_, clickedByColor, isRightClick) end end --- function for "draw 1 button" (that can be added via option panel) -function doDrawOne(_, color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[playerColor].seated and playerColor or color +-- click function for "draw 1 button" (that can be added via option panel) +function doDrawOne(_, clickedByColor) + updateMessageColor(clickedByColor) drawCardsWithReshuffle(1) end --- draw X cards (shuffle discards if necessary) +-- draws the specified amount of cards (and shuffles the discard if necessary) +---@param numCards number Number of cards to draw function drawCardsWithReshuffle(numCards) local deckAreaObjects = getDeckAreaObjects() @@ -1396,12 +1154,13 @@ function drawCardsWithReshuffle(numCards) end -- get the draw deck and discard pile objects and returns the references +---@return table: string-indexed table with references to the found objects function getDeckAreaObjects() local deckAreaObjects = {} for _, object in ipairs(searchDeckAndDiscardArea("isCardOrDeck")) do if self.positionToLocal(object.getPosition()).z > 0.5 then deckAreaObjects.discard = object - -- Norman Withers handling + -- Norman Withers handling elseif object.type == "Card" and not object.is_face_down then deckAreaObjects.topCard = object else @@ -1411,6 +1170,8 @@ function getDeckAreaObjects() return deckAreaObjects end +-- draws the specified number of cards (reshuffling of discard pile is handled separately) +---@param numCards number Number of cards to draw function drawCards(numCards) local deckAreaObjects = getDeckAreaObjects() if deckAreaObjects.draw then @@ -1449,31 +1210,211 @@ end function doDiscardOne() local hand = Player[playerColor].getHandObjects() if #hand == 0 then - broadcastToAll("Cannot discard from empty hand!", "Red") + broadcastToColor("Cannot discard from empty hand!", messageColor, "Red") else local choices = {} - for i = 1, #hand do - local notes = JSON.decode(hand[i].getGMNotes()) - if notes ~= nil then - if notes.hidden ~= true then + local hiddenCards = {} + local missingMetadataCards = {} + for i, handObj in ipairs(hand) do + if handObj.type == "Card" then + -- get a name for the card or use the index if unnamed + local name = handObj.getName() + if name == "" then + name = "Card " .. i + end + + -- check card for metadata + local md = JSON.decode(handObj.getGMNotes()) + if md == nil then + table.insert(missingMetadataCards, name) + elseif md.hidden or md.id == "52020" then + table.insert(hiddenCards, name) + else table.insert(choices, i) end - else - table.insert(choices, i) end end + -- print message with hidden cards + if #hiddenCards > 0 then + local cardList = concatenateListOfStrings(hiddenCards) + printToColor("Excluded (hidden): " .. cardList, messageColor) + end + + -- print message with missing metadata cards + if #missingMetadataCards > 0 then + local cardList = concatenateListOfStrings(missingMetadataCards) + printToColor("Excluded (missing data): " .. cardList, messageColor) + end + if #choices == 0 then - broadcastToAll("Hidden cards can't be randomly discarded.", "Orange") + broadcastToColor("Didn't find any eligible cards for random discarding.", messageColor, "Orange") return end - -- get a random non-hidden card (from the "choices" table) + -- get a random eligible card (from the "choices" table) local num = math.random(1, #choices) deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation()) + broadcastToAll(getColoredName(playerColor) .. " randomly discarded card " + .. choices[num] .. "/" .. #hand .. ".", "White") + end +end - local playerName = Player[playerColor].steam_name or playerColor - broadcastToAll(playerName .. " randomly discarded card " .. choices[num] .. "/" .. #hand .. ".", "White") +function concatenateListOfStrings(list) + local cardList + for _, cardName in ipairs(list) do + if not cardList then + cardList = "" + else + cardList = cardList .. ", " + end + cardList = cardList .. cardName + end + return cardList +end + +-- checks if DES is present +function checkForDES() + hasDES = false + for _, obj in ipairs(searchAroundSelf()) do + if obj.type == "Card" then + local cardMetadata = JSON.decode(obj.getGMNotes()) or {} + + -- position is used to exclude deck / discard + local cardPos = self.positionToLocal(obj.getPosition()) + if cardMetadata.id == "06159" and cardPos.x > -1 then + hasDES = true + break + end + end + end +end + +--------------------------------------------------------- +-- slot symbol displaying +--------------------------------------------------------- + +-- this will redraw the XML for the slot symbols based on the slotData table +function redrawSlotSymbols() + local xml = {} + local snapId = 0 + + -- use the snap point positions in the main play area for positions + for _, snap in ipairs(self.getSnapPoints()) do + if inArea(snap.position, MAIN_PLAY_AREA) then + snapId = snapId + 1 + local slotName = slotData[snapId] + + -- conversion from regular coordinates to XML + local x = snap.position.x * 100 + local y = snap.position.z * 100 + + -- XML for a single slot (panel with text in the special font) + local slotXML = { + tag = "Panel", + attributes = { + id = "slotPanel" .. snapId, + scale = "0.1 0.1 1", + width = "175", + height = "175", + position = x .. " " .. y .. " -11" + }, + children = { + { + tag = "Text", + attributes = { + id = "slot" .. snapId, + rotation = getSlotRotation(slotName), + fontSize = "145", + font = "font_arkhamicons", + color = "#414141CB", + text = slotNameToChar[slotName] + } + } + } + } + table.insert(xml, slotXML) + end + end + + self.UI.setXmlTable(xml) +end + +-- toggle the "slot editing mode" +function toggleSlotEditing(_, clickedByColor, isRightClick) + if isRightClick then + resetSlotSymbols() + return + end + + updateMessageColor(clickedByColor) + + -- toggle internal variable + currentlyEditingSlots = not currentlyEditingSlots + + if currentlyEditingSlots then + editButtonLabel("Edit Slots", "Stop editing") + broadcastToColor("Click on a slot symbol (or an empty slot) to edit it.", messageColor, "Orange") + addClickFunctionToSlots() + else + editButtonLabel("Stop editing", "Edit Slots") + redrawSlotSymbols() + end +end + +-- click function for slot symbols during the "slot editing mode" +function slotClickfunction(player, _, id) + local slotIndex = id:gsub("slotPanel", "") + slotIndex = tonumber(slotIndex) + + -- make a list of the table keys as options for the dialog box + local slotNames = {} + for slotName, _ in pairs(slotNameToChar) do + table.insert(slotNames, slotName) + end + + -- prompt player to choose symbol + player.showOptionsDialog("Choose Slot Symbol", slotNames, slotData[slotIndex], + function(chosenSlotName) + slotData[slotIndex] = chosenSlotName + + -- update slot symbol + self.UI.setAttribute("slot" .. slotIndex, "text", slotNameToChar[chosenSlotName]) + + -- update slot rotation + self.UI.setAttribute("slot" .. slotIndex, "rotation", getSlotRotation(chosenSlotName)) + end + ) +end + +-- helper function to rotate the left hand +function getSlotRotation(slotName) + if slotName == "Hand (left)" then + return "0 180 180" + else + return "0 0 180" + end +end + +-- reset the slot symbols by making a deep copy of the default data and redrawing +function resetSlotSymbols() + slotData = {} + for _, slotName in ipairs(defaultSlotData) do + table.insert(slotData, slotName) + end + + redrawSlotSymbols() + + -- need to re-add the click functions if currently in edit mode + if currentlyEditingSlots then + addClickFunctionToSlots() + end +end + +-- enables the click functions for editing +function addClickFunctionToSlots() + for i = 1, #slotData do + self.UI.setAttribute("slotPanel" .. i, "onClick", "slotClickfunction") end end @@ -1483,23 +1424,12 @@ end -- changes the player color function changeColor(clickedByColor) - local colorList = { - "White", - "Brown", - "Red", - "Orange", - "Yellow", - "Green", - "Teal", - "Blue", - "Purple", - "Pink" - } + local colorList = Player.getColors() -- remove existing colors from the list of choices for _, existingColor in ipairs(Player.getAvailableColors()) do for i, newColor in ipairs(colorList) do - if existingColor == newColor then + if existingColor == newColor or newColor == "Black" or newColor == "Grey" then table.remove(colorList, i) end end @@ -1523,7 +1453,7 @@ function changeColor(clickedByColor) end --------------------------------------------------------- --- playmat token spawning +-- playermat token spawning --------------------------------------------------------- -- Finds all customizable cards in this play area and updates their metadata based on the selections @@ -1538,16 +1468,14 @@ function syncAllCustomizableCards() end function syncCustomizableMetadata(card) - local cardMetadata = JSON.decode(card.getGMNotes()) or { } - if cardMetadata == nil or cardMetadata.customizations == nil then - return - end + local cardMetadata = JSON.decode(card.getGMNotes()) or {} + if cardMetadata == nil or cardMetadata.customizations == nil then return end + for _, upgradeSheet in ipairs(searchAroundSelf("isCard")) do - local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or { } + local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {} if upgradeSheetMetadata.id == (cardMetadata.id .. "-c") then for i, customization in ipairs(cardMetadata.customizations) do if customization.replaces ~= nil and customization.replaces.uses ~= nil then - -- Allowed use of call(), no APIs for individual cards if upgradeSheet.call("isUpgradeActive", i) then cardMetadata.uses = customization.replaces.uses card.setGMNotes(JSON.encode(cardMetadata)) @@ -1564,7 +1492,7 @@ function syncCustomizableMetadata(card) end function spawnTokensFor(object) - local extraUses = { } + local extraUses = {} if activeInvestigatorId == "03004" then extraUses["Charge"] = 1 end @@ -1581,26 +1509,18 @@ function onCollisionEnter(collisionInfo) -- only continue for cards if object.type ~= "Card" then return end - -- detect if "Dream-Enhancing Serum" is placed - if object.getName() == "Dream-Enhancing Serum" then isDES = true end - maybeUpdateActiveInvestigator(object) syncCustomizableMetadata(object) local localCardPos = self.positionToLocal(object.getPosition()) if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) + tokenSpawnTrackerApi.resetTokensSpawned(object) removeTokensFromObject(object) elseif shouldSpawnTokens(object) then spawnTokensFor(object) end end --- detect if "Dream-Enhancing Serum" is removed -function onCollisionExit(collisionInfo) - if collisionInfo.collision_object.getName() == "Dream-Enhancing Serum" then isDES = false end -end - -- checks if tokens should be spawned for the provided card function shouldSpawnTokens(card) if card.is_face_down then @@ -1618,15 +1538,15 @@ function shouldSpawnTokens(card) -- Spawn tokens for assets and events on the main area if inArea(localCardPos, MAIN_PLAY_AREA) and (metadata.type == "Asset" - or metadata.type == "Event") then + or metadata.type == "Event") then return true end -- Spawn tokens for all encounter types in the threat area if inArea(localCardPos, THREAT_AREA) and (metadata.type == "Treachery" - or metadata.type == "Enemy" - or metadata.weakness) then + or metadata.type == "Enemy" + or metadata.weakness) then return true end @@ -1638,7 +1558,7 @@ function onObjectEnterContainer(container, object) local localCardPos = self.positionToLocal(object.getPosition()) if inArea(localCardPos, DECK_DISCARD_AREA) then - tokenManager.resetTokensSpawned(object) + tokenSpawnTrackerApi.resetTokensSpawned(object) removeTokensFromObject(object) end end @@ -1654,14 +1574,13 @@ function removeTokensFromObject(object) for _, obj in ipairs(searchLib.onObject(object)) do if tokenChecker.isChaosToken(obj) then - chaosBagApi.returnChaosTokenToBag(obj) + chaosBagApi.returnChaosTokenToBag(obj, false) elseif obj.getGUID() ~= "4ee1f2" and -- table obj ~= self and obj.type ~= "Deck" and obj.type ~= "Card" and obj.memo ~= nil and - obj.getLock() == false and - obj.getDescription() ~= "Action Token" then + obj.getLock() == false then ownedObjects.Trash.putObject(obj) end end @@ -1671,77 +1590,147 @@ end -- investigator ID grabbing and skill tracker --------------------------------------------------------- +-- updates the internal investigator id and action tokens if an investigator card is detected +---@param card tts__Object Card that might be an investigator function maybeUpdateActiveInvestigator(card) if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end local notes = JSON.decode(card.getGMNotes()) - local class + local extraToken if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then if notes.id == activeInvestigatorId then return end - class = notes.class + activeInvestigatorClass = notes.class activeInvestigatorId = notes.id + extraToken = notes.extraToken ownedObjects.InvestigatorSkillTracker.call("updateStats", { notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons }) + updateTexture() elseif activeInvestigatorId ~= "00000" then - class = "Neutral" + activeInvestigatorClass = "Neutral" activeInvestigatorId = "00000" - ownedObjects.InvestigatorSkillTracker.call("updateStats", {1, 1, 1, 1}) + ownedObjects.InvestigatorSkillTracker.call("updateStats", { 1, 1, 1, 1 }) + updateTexture() else return end - -- change state of action tokens - local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1}) - local smallToken = nil - local STATE_TABLE = { - ["Guardian"] = 1, - ["Seeker"] = 2, - ["Rogue"] = 3, - ["Mystic"] = 4, - ["Survivor"] = 5, - ["Neutral"] = 6 - } + -- set proper scale for investigators + local cardData = card.getData() + if cardData["SidewaysCard"] == true then + -- 115% for easier readability + card.setScale({ 1.15, 1, 1.15 }) + else + -- Zoop-exported investigators are horizontal cards and TTS scales them differently + card.setScale({ 0.8214, 1, 0.8214 }) + end - for _, obj in ipairs(search) do - if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then - if obj.getScale().x < 0.4 then - smallToken = obj + -- remove old action tokens + for _, obj in ipairs(searchAroundSelf("isUniversalToken")) do + obj.destruct() + end + + -- spawn three regular action tokens (investigator specific one in the bottom spot) + for i = 1, 3 do + local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(pos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = activeInvestigatorClass }) + end) + end + + -- spawn additional token (maybe specific for investigator) + if extraToken and extraToken ~= "None" then + -- local positions + local tokenSpawnPos = { + action = { + Vector(-0.86, 0, -0.28), -- left of the regular three actions + Vector(-1.54, 0, -0.28), -- right of the regular three actions + }, + ability = { + Vector(-1, 0, 0.118), -- bottom left corner of the investigator card + Vector(-1, 0, -0.118), -- top left corner of the investigator card + } + } + + -- spawn tokens (split string by "|") + local count = { action = 0, ability = 0 } + for str in string.gmatch(extraToken, "([^|]+)") do + local type = "action" + if str == "FreeTrigger" or str == "Reaction" then + type = "ability" + end + + count[type] = count[type] + 1 + if count[type] > 2 then + printToColor("More than two extra tokens of the same type are not supported.", playerColor) else - setObjectState(obj, STATE_TABLE[class]) + local localSpawnPos = tokenSpawnPos[type][count[type]] + local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0)) + + tokenManager.spawnToken(globalSpawnPos, "universalActionAbility", self.getRotation(), + function(spawned) + spawned.call("updateClassAndSymbol", { class = activeInvestigatorClass, symbol = str }) + end) end end end - - -- update the small token with special action for certain investigators - local SPECIAL_ACTIONS = { - ["04002"] = 8, -- Ursula Downs - ["01002"] = 9, -- Daisy Walker - ["01502"] = 9, -- Daisy Walker - ["01002-pb"] = 9, -- Daisy Walker - ["06003"] = 10, -- Tony Morgan - ["04003"] = 11, -- Finn Edwards - ["08016"] = 14 -- Bob Jenkins - } - - if smallToken ~= nil then - setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class]) - end end -function setObjectState(obj, stateId) - if obj.getStateId() ~= stateId then obj.setState(stateId) end +-- updates the texture of the playermat +---@param overrideName? string Force a specific texture +function updateTexture(overrideName) + local name = "Neutral" + + -- use class specific texture if enabled + if isClassTextureEnabled then + name = activeInvestigatorClass + end + + -- get new texture URL + local newUrl = nameToTexture[name] + + -- override name if valid + if nameToTexture[overrideName] then + newUrl = nameToTexture[overrideName] + end + + -- apply texture + local customInfo = self.getCustomObject() + if customInfo.image ~= newUrl then + -- temporarily lock objects so they don't fall through the mat + local objectsToUnlock = {} + for _, obj in ipairs(searchAroundSelf()) do + if not obj.getLock() then + obj.setLock(true) + table.insert(objectsToUnlock, obj) + end + end + + self.script_state = onSave() + customInfo.image = newUrl + self.setCustomObject(customInfo) + local reloadedMat = self.reload() + + -- unlock objects when mat is reloaded + Wait.condition(function() + for _, obj in ipairs(objectsToUnlock) do + obj.setLock(false) + end + end, function() return reloadedMat.loading_custom == false end) + end end --------------------------------------------------------- -- manipulation of owned objects --------------------------------------------------------- --- updates the specific owned counter +-- updates the specified owned counter ---@param param table Contains the information to update: --- type: String Counter to target --- newValue: Number Value to set the counter to @@ -1755,20 +1744,21 @@ function updateCounter(param) end end --- returns the resource counter amount +-- get the value the specified owned counter ---@param type string Counter to target +---@return number: Counter value function getCounterValue(type) return ownedObjects[type].getVar("val") end -- set investigator skill tracker to "1, 1, 1, 1" function resetSkillTracker() - local obj = ownedObjects.InvestigatorSkillTracker - if obj ~= nil then - obj.call("updateStats", { 1, 1, 1, 1 }) - else - printToAll("Skill tracker for " .. matColor .. " playmat could not be found.", "Yellow") - end + local obj = ownedObjects.InvestigatorSkillTracker + if obj ~= nil then + obj.call("updateStats", { 1, 1, 1, 1 }) + else + printToAll("Skill tracker for " .. matColor .. " playermat could not be found.", "Yellow") + end end --------------------------------------------------------- @@ -1780,32 +1770,53 @@ function drawChaosTokenButton(_, _, isRightClick) end function drawEncounterCard(_, _, isRightClick) - mythosAreaApi.drawEncounterCard(self, isRightClick) + local drawPos = getEncounterCardDrawPosition(not isRightClick) + mythosAreaApi.drawEncounterCard(matColor, drawPos) end function returnGlobalDiscardPosition() return self.positionToWorld(DISCARD_PILE_POSITION) end --- Sets this playermat's draw 1 button to visible +function returnGlobalDrawPosition() + return self.positionToWorld(DRAW_DECK_POSITION) +end + +-- returns the position for encounter card drawing +---@param stack boolean If true, returns the leftmost position instead of the first empty from the right +function getEncounterCardDrawPosition(stack) + local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION) + + -- maybe override position with first empty slot in threat area (right to left) + if not stack then + local searchPos = Vector(-0.91, 0.5, -0.625) + for i = 1, 5 do + local globalSearchPos = self.positionToWorld(searchPos) + local searchResult = searchLib.atPosition(globalSearchPos, "isCardOrDeck") + if #searchResult == 0 then + drawPos = globalSearchPos + break + else + searchPos.x = searchPos.x + 0.455 + end + end + end + + return drawPos +end + +-- creates / removes the draw 1 button ---@param visible boolean Whether the draw 1 button should be visible function showDrawButton(visible) isDrawButtonVisible = visible - -- create the "Draw 1" button if isDrawButtonVisible then - self.createButton({ - label = "Draw 1", - click_function = "doDrawOne", - function_owner = self, - position = { 1.84, 0.1, -0.36 }, - scale = { 0.12, 0.12, 0.12 }, - width = 800, - height = 280, - font_size = 180 - }) - - -- remove the "Draw 1" button + -- Draw 1 button: modified default data + buttonParameters.label = "Draw 1" + buttonParameters.click_function = "doDrawOne" + buttonParameters.tooltip = "" + buttonParameters.position.z = -0.35 + self.createButton(buttonParameters) else local buttons = self.getButtons() for i = 1, #buttons do @@ -1816,18 +1827,18 @@ function showDrawButton(visible) end end --- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues +-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues ---@param showCounter boolean Whether the clickable clue counter should be visible function clickableClues(showCounter) local clickerPos = ownedObjects.ClickableClueCounter.getPosition() local clueCount = 0 - + -- move clue counters local modY = showCounter and 0.525 or -0.525 ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0)) if showCounter then - -- current clue count + -- get current clue count clueCount = ownedObjects.ClueCounter.getVar("exposedValue") -- remove clues @@ -1836,11 +1847,11 @@ function clickableClues(showCounter) -- set value for clue clickers ownedObjects.ClickableClueCounter.call("updateVal", clueCount) else - -- current clue count + -- get current clue count clueCount = ownedObjects.ClickableClueCounter.getVar("val") -- spawn clues - local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7}) + local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 }) for i = 1, clueCount do pos.y = pos.y + 0.045 * i tokenManager.spawnToken(pos, "clue", self.getRotation()) @@ -1848,6 +1859,14 @@ function clickableClues(showCounter) end end +-- Toggles the use of class textures +---@param state boolean Whether the class texture should be used or not +function useClassTexture(state) + if state == isClassTextureEnabled then return end + isClassTextureEnabled = state + updateTexture() +end + -- removes all clues (moving tokens to the trash and setting counters to 0) function removeClues() ownedObjects.ClueCounter.call("removeAllClues", ownedObjects.Trash) @@ -1871,8 +1890,7 @@ end function setLimitSnapsByType(matchTypes) local snaps = self.getSnapPoints() for i, snap in ipairs(snaps) do - local snapPos = snap.position - if inArea(snapPos, MAIN_PLAY_AREA) then + if inArea(snap.position, MAIN_PLAY_AREA) then local snapTags = snaps[i].tags if matchTypes then if snapTags == nil then @@ -1884,7 +1902,7 @@ function setLimitSnapsByType(matchTypes) snaps[i].tags = nil end end - if inArea(snapPos, INVESTIGATOR_AREA) then + if inArea(snap.position, INVESTIGATOR_AREA) then local snapTags = snaps[i].tags if matchTypes then if snapTags == nil then @@ -1900,16 +1918,15 @@ function setLimitSnapsByType(matchTypes) self.setSnapPoints(snaps) end --- 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 tts__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 --- bounds definition. +---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition. ---@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 -- called by custom data helpers to add player card data @@ -1919,66 +1936,62 @@ function updatePlayerCards(args) local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA") tokenManager.addPlayerCardData(playerCardData) end -end) -__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local DeckLib = {} - local searchLib = require("util/SearchLib") - -- places a card/deck at a position or merges into an existing deck - ---@param obj tts__Object Object to move - ---@param pos table New position for the object - ---@param rot table New rotation for the object (optional) - DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot) - if obj == nil or pos == nil then return end - - -- search the new position for existing card/deck - local searchResult = searchLib.atPosition(pos, "isCardOrDeck") - - -- get new position - local newPos - local offset = 0.5 - if #searchResult == 1 then - local bounds = searchResult[1].getBounds() - newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) - else - newPos = Vector(pos) + Vector(0, offset, 0) - end - - -- allow moving the objects smoothly out of the hand - obj.use_hands = false - - if rot then - obj.setRotationSmooth(rot, false, true) - end - obj.setPositionSmooth(newPos, false, true) - - -- continue if the card stops smooth moving - Wait.condition( - function() - obj.use_hands = true - -- this avoids a TTS bug that merges unrelated cards that are not resting - 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 - end, - function() return not obj.isSmoothMoving() end, 3) +-- returns the colored steam name or color +function getColoredName(playerColor) + local displayName = playerColor + if Player[playerColor].steam_name then + displayName = Player[playerColor].steam_name end - return DeckLib + -- add bb-code + return "[" .. Color.fromString(playerColor):toHex() .. "]" .. displayName .. "[-]" +end +end) +__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local MythosAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getMythosArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") + end + + ---@return any: Table of chaos token metadata (if provided through scenario reference card) + MythosAreaApi.returnTokenData = function() + return getMythosArea().call("returnTokenData") + end + + ---@return any: Object reference to the encounter deck + MythosAreaApi.getEncounterDeck = function() + return getMythosArea().call("getEncounterDeck") + end + + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) + end + + -- reshuffle the encounter deck + MythosAreaApi.reshuffleEncounterDeck = function() + getMythosArea().call("reshuffleEncounterDeck") + end + + return MythosAreaApi end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -2002,7 +2015,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -2019,25 +2032,775 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end return SearchLib end end) +__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local CHAOS_TOKEN_NAMES = { + ["Elder Sign"] = true, + ["+1"] = true, + ["0"] = true, + ["-1"] = true, + ["-2"] = true, + ["-3"] = true, + ["-4"] = true, + ["-5"] = true, + ["-6"] = true, + ["-7"] = true, + ["-8"] = true, + ["Skull"] = true, + ["Cultist"] = true, + ["Tablet"] = true, + ["Elder Thing"] = true, + ["Auto-fail"] = true, + ["Bless"] = true, + ["Curse"] = true, + ["Frost"] = true + } + + local TokenChecker = {} + + -- returns true if the passed object is a chaos token (by name) + TokenChecker.isChaosToken = function(obj) + if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then + return true + else + return false + end + end + + return TokenChecker +end +end) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") + else + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } + end + end + + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end + end + return result + end + + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) + local objList = {} + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) + end + end + return objList + end + + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end + end + + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end + end + + return PlayermatApi +end +end) +__bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local ChaosBagApi = {} + + -- respawns the chaos bag with a new state of tokens + ---@param tokenList table List of chaos token ids + ChaosBagApi.setChaosBagState = function(tokenList) + Global.call("setChaosBagState", tokenList) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getChaosBagState = function() + local chaosBagContentsCatcher = Global.call("getChaosBagState") + local chaosBagContents = {} + for _, v in ipairs(chaosBagContentsCatcher) do + table.insert(chaosBagContents, v) + end + return chaosBagContents + end + + -- checks scripting zone for chaos bag (also called by a lot of objects!) + ChaosBagApi.findChaosBag = function() + return Global.call("findChaosBag") + end + + -- returns a table of object references to the tokens in play (does not include sealed tokens!) + ChaosBagApi.getTokensInPlay = function() + return Global.call("getChaosTokensinPlay") + end + + -- returns all sealed tokens on cards to the chaos bag + ---@param playerColor string Color of the player to show the broadcast to + ChaosBagApi.releaseAllSealedTokens = function(playerColor) + Global.call("releaseAllSealedTokens", playerColor) + end + + -- returns all drawn tokens to the chaos bag + ChaosBagApi.returnChaosTokens = function() + Global.call("returnChaosTokens") + end + + -- removes the specified chaos token from the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.removeChaosToken = function(id) + Global.call("removeChaosToken", id) + end + + -- returns a chaos token to the bag and calls all relevant functions + ---@param token tts__Object Chaos token to return + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) + end + + -- spawns the specified chaos token and puts it into the chaos bag + ---@param id string ID of the chaos token + ChaosBagApi.spawnChaosToken = function(id) + Global.call("spawnChaosToken", id) + end + + -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens + -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the + -- contents of the bag should check this method before doing so. + -- This method will broadcast a message to all players if the bag is being searched. + ---@return any: True if the bag is manipulated, false if it should be blocked. + ChaosBagApi.canTouchChaosTokens = function() + return Global.call("canTouchChaosTokens") + end + + -- draws a chaos token to a playermat + ---@param mat tts__Object Playermat that triggered this + ---@param drawAdditional boolean Controls whether additional tokens should be drawn + ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag + ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) + end + + -- returns a Table List of chaos token ids in the current chaos bag + -- requires copying the data into a new table because TTS is weird about handling table return values in Global + ChaosBagApi.getIdUrlMap = function() + return Global.getTable("ID_URL_MAP") + end + + return ChaosBagApi +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) +__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local DeckLib = {} + local searchLib = require("util/SearchLib") + + -- places a card/deck at a position or merges into an existing deck below + ---@param objOrTable tts__Object|table Object or table of objects to move + ---@param pos table New position for the object + ---@param rot? table New rotation for the object + ---@param below? boolean Should the object be placed below an existing deck? + DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) + if objOrTable == nil or pos == nil then return end + + -- handle 'objOrTable' parameter + local objects = {} + if type(objOrTable) == "table" then + objects = objOrTable + else + table.insert(objects, objOrTable) + end + + -- search the new position for existing card/deck + local searchResult = searchLib.atPosition(pos, "isCardOrDeck") + local targetObj + + -- get new position + local offset = 0.5 + local newPos = Vector(pos) + Vector(0, offset, 0) + + if #searchResult == 1 then + targetObj = searchResult[1] + local bounds = targetObj.getBounds() + if below then + newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) + else + newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) + end + end + + -- process objects in reverse order + for i = #objects, 1, -1 do + local obj = objects[i] + -- add a 0.1 delay for each object (for animation purposes) + Wait.time(function() + -- allow moving smoothly out of hand and temporarily lock it + obj.setLock(true) + obj.use_hands = false + + if rot then + obj.setRotationSmooth(rot, false, true) + end + obj.setPositionSmooth(newPos, false, true) + + -- wait for object to finish movement (or 2 seconds) + Wait.condition( + function() + -- revert toggles + obj.setLock(false) + obj.use_hands = true + + -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting + if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then + targetObj = targetObj.putObject(obj) + else + targetObj = obj + end + end, + -- check state of the object (make sure it's not moving) + function() return obj.isDestroyed() or not obj.isSmoothMoving() end, + 2) + end, (#objects- i) * 0.1) + end + end + + return DeckLib +end +end) +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + end + + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + end + + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") + end + + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) + end + + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) + end + + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) + end + + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) + end + + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi +end +end) return __bundle_require("__root") diff --git a/unpacked/Custom_Tile Playermat 4 Red 0840d5.yaml b/unpacked/Custom_Tile Playermat 4 Red 0840d5.yaml index bf985f594..d9935f6df 100644 --- a/unpacked/Custom_Tile Playermat 4 Red 0840d5.yaml +++ b/unpacked/Custom_Tile Playermat 4 Red 0840d5.yaml @@ -8,61 +8,61 @@ AttachedSnapPoints: y: 0.1 z: 0.12 Tags: - - ActionToken + - UniversalToken - Position: x: -0.86 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1 + x: -1.03 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.18 + x: -1.2 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -1.36 + x: -1.37 y: 0.1 z: -0.28 Tags: - - ActionToken + - UniversalToken - Position: - x: -0.63 + x: -1.54 y: 0.1 - z: 0.55 + z: -0.28 + Tags: + - UniversalToken +- Position: + x: 1.76 + y: 0.1 + z: 0.04 Tags: - Asset - Position: - x: -0.62 + x: 1.37 y: 0.1 - z: 0.02 + z: 0.04 Tags: - Asset - Position: - x: -0.18 + x: 0.98 + y: 0.1 + z: 0.04 + Tags: + - Asset +- Position: + x: 0.6 y: 0.1 z: 0.03 Tags: - Asset -- Position: - x: -0.17 - y: 0.1 - z: 0.55 - Tags: - - Asset -- Position: - x: 0.21 - y: 0.1 - z: 0.56 - Tags: - - Asset - Position: x: 0.22 y: 0.1 @@ -70,39 +70,15 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 0.6 + x: -0.18 y: 0.1 z: 0.03 Tags: - Asset - Position: - x: 0.6 + x: -0.62 y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.56 - Tags: - - Asset -- Position: - x: 0.98 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.04 - Tags: - - Asset -- Position: - x: 1.37 - y: 0.1 - z: 0.56 + z: 0.02 Tags: - Asset - Position: @@ -112,9 +88,39 @@ AttachedSnapPoints: Tags: - Asset - Position: - x: 1.76 + x: 1.37 y: 0.1 - z: 0.04 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.98 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.6 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: 0.21 + y: 0.1 + z: 0.56 + Tags: + - Asset +- Position: + x: -0.17 + y: 0.1 + z: 0.55 + Tags: + - Asset +- Position: + x: -0.63 + y: 0.1 + z: 0.55 Tags: - Asset - Position: @@ -208,7 +214,7 @@ CustomImage: Type: 3 ImageScalar: 1 ImageSecondaryURL: '' - ImageURL: http://cloud-3.steamusercontent.com/ugc/2037357630681963618/E7271737B19CE0BFAAA382BEEEF497FE3E06ECC1/ + ImageURL: http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/ WidthScale: 0 Description: '' DragSelectable: true @@ -222,7 +228,8 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Tile Playermat 4 Red 0840d5.ttslua' -LuaScriptState: '{"activeInvestigatorId":"00000","isDrawButtonVisible":false,"playerColor":"Red"}' +LuaScriptState: '{"activeInvestigatorClass":"Neutral","activeInvestigatorId":"00000","isClassTextureEnabled":true,"isDrawButtonVisible":false,"playerColor":"Red","slotData":["any","any","any","Tarot","Hand + (left)","Hand (right)","Ally","any","any","any","Accessory","Arcane","Arcane","Body"]}' MeasureMovement: false Memo: Red Name: Custom_Tile diff --git a/unpacked/Custom_Tile Search-A-Card 24051a.ttslua b/unpacked/Custom_Tile Search-A-Card 24051a.ttslua index 07e15ee21..bb1281434 100644 --- a/unpacked/Custom_Tile Search-A-Card 24051a.ttslua +++ b/unpacked/Custom_Tile Search-A-Card 24051a.ttslua @@ -44,126 +44,6 @@ end)(nil) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/CardSearch") end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) -__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local AllCardsBagApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - local function getAllCardsBag() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag") - end - - -- Returns a specific card from the bag, based on ArkhamDB ID - ---@param id table String ID of the card to retrieve - ---@return table table - -- If the indexes are still being constructed, an empty table is - -- returned. Otherwise, a single table with the following fields - -- cardData: TTS object data, suitable for spawning the card - -- cardMetadata: Table of parsed metadata - AllCardsBagApi.getCardById = function(id) - return getAllCardsBag().call("getCardById", {id = id}) - end - - -- Gets a random basic weakness from the bag. Once a given ID has been returned - -- it will be removed from the list and cannot be selected again until a reload - -- occurs or the indexes are rebuilt, which will refresh the list to include all - -- weaknesses. - ---@return string: ID of the selected weakness. - AllCardsBagApi.getRandomWeaknessId = function() - return getAllCardsBag().call("getRandomWeaknessId") - end - - AllCardsBagApi.isIndexReady = function() - return getAllCardsBag().call("isIndexReady") - end - - -- Called by Hotfix bags when they load. If we are still loading indexes, then - -- the all cards and hotfix bags are being loaded together, and we can ignore - -- this call as the hotfix will be included in the initial indexing. If it is - -- called once indexing is complete it means the hotfix bag has been added - -- later, and we should rebuild the index to integrate the hotfix bag. - AllCardsBagApi.rebuildIndexForHotfix = function() - return getAllCardsBag().call("rebuildIndexForHotfix") - end - - -- Searches the bag for cards which match the given name and returns a list. Note that this is - -- an O(n) search without index support. It may be slow. - ---@param name string or string fragment to search for names - ---@param exact boolean Whether the name match should be exact - AllCardsBagApi.getCardsByName = function(name, exact) - return getAllCardsBag().call("getCardsByName", {name = name, exact = exact}) - end - - AllCardsBagApi.isBagPresent = function() - return getAllCardsBag() and true - end - - -- Returns a list of cards from the bag matching a class and level (0 or upgraded) - ---@param class string class to retrieve ("Guardian", "Seeker", etc) - ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0 - ---@return table: If the indexes are still being constructed, returns an empty table. - -- Otherwise, a list of tables, each with the following fields - -- cardData: TTS object data, suitable for spawning the card - -- cardMetadata: Table of parsed metadata - AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) - return getAllCardsBag().call("getCardsByClassAndLevel", {class = class, upgraded = upgraded}) - end - - AllCardsBagApi.getCardsByCycle = function(cycle) - return getAllCardsBag().call("getCardsByCycle", cycle) - end - - AllCardsBagApi.getUniqueWeaknesses = function() - return getAllCardsBag().call("getUniqueWeaknesses") - end - - return AllCardsBagApi -end -end) __bundle_register("playercards/CardSearch", function(require, _LOADED, __bundle_register, __bundle_modules) require("playercards/PlayerCardSpawner") @@ -190,7 +70,9 @@ inputParameters.scale = { 0.1, 1, 0.1 } inputParameters.color = { 0.9, 0.7, 0.5 } inputParameters.font_color = { 0, 0, 0 } -function onSave() return JSON.encode({ spawnAll, searchExact, inputParameters.value }) end +function onSave() + return JSON.encode({ spawnAll, searchExact, inputParameters.value }) +end function onLoad(savedData) local loadedData = JSON.decode(savedData) @@ -287,6 +169,104 @@ function startSearch() Spawner.spawnCards(cardList, pos, rot, true) end end) +__bundle_register("playercards/AllCardsBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local AllCardsBagApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getAllCardsBag() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "AllCardsBag") + end + + -- internal function to create a copy of the table to avoid operating on variables owned by different objects + local function returnCopyOfList(data) + local copiedList = {} + for _, id in ipairs(data) do + table.insert(copiedList, id) + end + return copiedList + end + + -- Returns a specific card from the bag, based on ArkhamDB ID + ---@param id string ID of the card to retrieve + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a single table with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardById = function(id) + return getAllCardsBag().call("getCardById", { id = id }) + end + + -- Gets a random basic weakness from the bag. Once a given ID has been returned it + -- will be removed from the list and cannot be selected again until a reload occurs + -- or the indexes are rebuilt, which will refresh the list to include all weaknesses. + ---@return string: ID of the selected weakness + AllCardsBagApi.getRandomWeaknessId = function() + return getAllCardsBag().call("getRandomWeaknessId") + end + + AllCardsBagApi.isIndexReady = function() + return getAllCardsBag().call("isIndexReady") + end + + -- Called by Hotfix bags when they load. If we are still loading indexes, then + -- the all cards and hotfix bags are being loaded together, and we can ignore + -- this call as the hotfix will be included in the initial indexing. If it is + -- called once indexing is complete it means the hotfix bag has been added + -- later, and we should rebuild the index to integrate the hotfix bag. + AllCardsBagApi.rebuildIndexForHotfix = function() + getAllCardsBag().call("rebuildIndexForHotfix") + end + + -- Searches the bag for cards which match the given name and returns a list. + -- Note that this is an O(n) search without index support. It may be slow. + ---@param name string or string fragment to search for names + ---@param exact boolean Whether the name match should be exact + AllCardsBagApi.getCardsByName = function(name, exact) + return returnCopyOfList(getAllCardsBag().call("getCardsByName", { name = name, exact = exact })) + end + + AllCardsBagApi.isBagPresent = function() + return getAllCardsBag() and true + end + + -- Returns a list of cards from the bag matching a class and level (0 or upgraded) + ---@param class string class to retrieve ("Guardian", "Seeker", etc) + ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0 + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a list of tables, each with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded) + return returnCopyOfList(getAllCardsBag().call("getCardsByClassAndLevel", { class = class, upgraded = upgraded })) + end + + -- Returns a list of cards from the bag matching a cycle + ---@param cycle string Cycle to retrieve ("The Scarlet Keys" etc.) + ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID + ---@return table: If the indexes are still being constructed, returns an empty table. + -- Otherwise, a list of tables, each with the following fields + -- data: TTS object data, suitable for spawning the card + -- metadata: Table of parsed metadata + AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata) + return returnCopyOfList(getAllCardsBag().call("getCardsByCycle", { cycle = cycle, sortByMetadata = sortByMetadata })) + end + + -- Constructs a list of available basic weaknesses by starting with the full pool of basic + -- weaknesses then removing any which are currently in the play or deck construction areas + ---@param traits? string Trait(s) to use as filter + ---@return table: Array of weakness IDs which are valid to choose from + AllCardsBagApi.buildAvailableWeaknesses = function(traits) + return returnCopyOfList(getAllCardsBag().call("buildAvailableWeaknesses", traits)) + end + + AllCardsBagApi.getUniqueWeaknesses = function() + return returnCopyOfList(getAllCardsBag().call("getUniqueWeaknesses")) + end + + return AllCardsBagApi +end +end) __bundle_register("playercards/PlayerCardSpawner", function(require, _LOADED, __bundle_register, __bundle_modules) -- Amount to shift for the next card (zShift) or next row of cards (xShift) -- Note that the table rotation is weird, and the X axis is vertical while the @@ -296,8 +276,8 @@ local SPREAD_X_SHIFT = -3.66 Spawner = { } --- Spawns a list of cards at the given position/rotation. This will separate cards by size - --- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If +-- Spawns a list of cards at the given position/rotation. This will separate cards by size - +-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If -- there are different types, the provided callback will be called once for each type as it spawns -- either a card or deck. ---@param cardList table A list of Player Card data structures (data/metadata) @@ -306,7 +286,7 @@ Spawner = { } ---@param sort boolean True if this list of cards should be sorted before spawning ---@param callback? function Callback to be called after the card/deck spawns. Spawner.spawnCards = function(cardList, pos, rot, sort, callback) - if (sort) then + if sort then table.sort(cardList, Spawner.cardComparator) end @@ -315,16 +295,19 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback) local investigatorCards = { } for _, card in ipairs(cardList) do - if (card.metadata.type == "Investigator") then + if card.metadata.type == "Investigator" then table.insert(investigatorCards, card) - elseif (card.metadata.type == "Minicard") then + elseif card.metadata.type == "Minicard" then + -- set proper scale for minicards + card.data.Transform.scaleX = 0.6 + card.data.Transform.scaleZ = 0.6 table.insert(miniCards, card) else table.insert(standardCards, card) end end - -- Spawn each of the three types individually. Each Y position shift accounts for the thickness - -- of the spawned deck + + -- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck local position = { x = pos.x, y = pos.y, z = pos.z } Spawner.spawn(investigatorCards, position, rot, callback) @@ -336,13 +319,13 @@ Spawner.spawnCards = function(cardList, pos, rot, sort, callback) end Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback) - if (sort) then + if sort then table.sort(cardList, Spawner.cardComparator) end local position = { x = startPos.x, y = startPos.y, z = startPos.z } -- Special handle the first row if we have less than a full single row, but only if there's a - -- reasonable max column count. Single-row spreads will send a large value for maxCols + -- reasonable max column count. Single-row spreads will send a large value for maxCols if maxCols < 100 and #cardList < maxCols then position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT) end @@ -365,7 +348,7 @@ Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callb end end --- Spawn a specific list of cards. This method is for internal use and should not be called +-- Spawn a specific list of cards. This method is for internal use and should not be called -- directly, use spawnCards instead. ---@param cardList table A list of Player Card data structures (data/metadata) ---@param pos table Position where the cards should be spawned (global) @@ -380,25 +363,18 @@ Spawner.spawn = function(cardList, pos, rot, callback) if cardList[1].data.SidewaysCard then rot = { rot.x, rot.y - 90, rot.z } end - spawnObjectData({ + return spawnObjectData({ data = cardList[1].data, position = pos, rotation = rot, callback_function = callback }) - return end -- For multiple cards, construct a deck and spawn that - local deck = Spawner.buildDeckDataTemplate() - - -- Decks won't inherently scale to the cards in them. The card list being spawned should be all - -- the same type/size by this point, so use the first card to set the size - deck.Transform = { - scaleX = cardList[1].data.Transform.scaleX, - scaleY = 1, - scaleZ = cardList[1].data.Transform.scaleZ - } + local deckScaleX = cardList[1].data.Transform.scaleX + local deckScaleZ = cardList[1].data.Transform.scaleZ + local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ) local sidewaysDeck = true for _, spawnCard in ipairs(cardList) do @@ -413,7 +389,7 @@ Spawner.spawn = function(cardList, pos, rot, callback) rot = { rot.x, rot.y - 90, rot.z } end - spawnObjectData({ + return spawnObjectData({ data = deck, position = pos, rotation = rot, @@ -421,12 +397,12 @@ Spawner.spawn = function(cardList, pos, rot, callback) }) end --- Inserts a card into the given deck. This does three things: +-- Inserts a card into the given deck. This does three things: -- 1. Add the card's data to ContainedObjects -- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's --- ID list. Note that the deck's ID list is "DeckIDs" even though it +-- ID list. Note that the deck's ID list is "DeckIDs" even though it -- contains a list of card Ids --- 3. Extract the card's CustomDeck table and add it to the deck. The deck's +-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's -- "CustomDeck" field is a list of all CustomDecks used by cards within the -- deck, keyed by the DeckID and referencing the custom deck table ---@param deck table TTS deck data structure to add to @@ -466,20 +442,22 @@ end -- creates a new table on each call without using metatables or previous -- definitions because we can't be sure that TTS doesn't modify the structure ---@return table deck Table containing the minimal TTS deck data structure -Spawner.buildDeckDataTemplate = function() +Spawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ) local deck = {} deck.Name = "Deck" - -- Card data. DeckIDs and CustomDeck entries will be built from the cards + -- Card data. DeckIDs and CustomDeck entries will be built from the cards deck.ContainedObjects = {} deck.DeckIDs = {} deck.CustomDeck = {} -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here + -- Decks won't inherently scale to the cards in them. The card list being spawned should be all + -- the same type/size by this point, so use the first card to set the size deck.Transform = { - scaleX = 1, + scaleX = deckScaleX or 1, scaleY = 1, - scaleZ = 1, + scaleZ = deckScaleZ or 1, } return deck @@ -491,7 +469,7 @@ end ---@return string id >= startId Spawner.findNextAvailableId = function(objectTable, startId) local id = startId - while (objectTable[id] ~= nil) do + while objectTable[id] ~= nil do id = tostring(tonumber(id) + 1) end return id @@ -510,14 +488,14 @@ Spawner.getpbcn = function(metadata) end end --- Comparison function used to sort the cards in a deck. Groups bonded or +-- Comparison function used to sort the cards in a deck. Groups bonded or -- permanent cards first, then sorts within theose types by name/subname. -- Normal cards will sort in standard alphabetical order, while -- permanent/bonded/customizable will be in reverse alphabetical order. -- -- Since cards spawn in the order provided by this comparator, with the first -- cards ending up at the bottom of a pile, this ordering will spawn in reverse --- alphabetical order. This presents the cards in order for non-face-down +-- alphabetical order. This presents the cards in order for non-face-down -- areas, and presents them in order when Searching the face-down deck. Spawner.cardComparator = function(card1, card2) local pbcn1 = Spawner.getpbcn(card1.metadata) @@ -538,4 +516,56 @@ Spawner.cardComparator = function(card1, card2) end end end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Tile Search-A-Card 24051a.yaml b/unpacked/Custom_Tile Search-A-Card 24051a.yaml index cb92e8916..b8427f11e 100644 --- a/unpacked/Custom_Tile Search-A-Card 24051a.yaml +++ b/unpacked/Custom_Tile Search-A-Card 24051a.yaml @@ -4,9 +4,9 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - b: 1 - g: 1 - r: 1 + b: 0 + g: 0 + r: 0 CustomImage: CustomTile: Stackable: false @@ -43,7 +43,7 @@ Tooltip: true Transform: posX: 60 posY: 1.48 - posZ: 56 + posZ: 57 rotX: 0 rotY: 270 rotZ: 0 diff --git a/unpacked/Custom_Tile Token Remover 0a5a29.ttslua b/unpacked/Custom_Tile Token Remover 0a5a29.ttslua index b670f04eb..a2b59aa32 100644 --- a/unpacked/Custom_Tile Token Remover 0a5a29.ttslua +++ b/unpacked/Custom_Tile Token Remover 0a5a29.ttslua @@ -45,11 +45,11 @@ __bundle_register("__root", function(require, _LOADED, __bundle_register, __bund require("util/TokenRemover") end) __bundle_register("util/TokenRemover", function(require, _LOADED, __bundle_register, __bundle_modules) -local zone = nil +local zone -- general code function onSave() - return JSON.encode(zone and zone.getGUID() or nil) + return JSON.encode(zone and zone.getGUID()) end function onLoad(savedData) diff --git a/unpacked/Custom_Tile Token Remover 0a5a29.yaml b/unpacked/Custom_Tile Token Remover 0a5a29.yaml index 2b1961ac3..a4b5ff7cd 100644 --- a/unpacked/Custom_Tile Token Remover 0a5a29.yaml +++ b/unpacked/Custom_Tile Token Remover 0a5a29.yaml @@ -4,9 +4,9 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - b: 1 - g: 1 - r: 1 + b: 0 + g: 0 + r: 0 CustomImage: CustomTile: Stackable: false @@ -17,7 +17,8 @@ CustomImage: ImageSecondaryURL: http://cloud-3.steamusercontent.com/ugc/1767069252728653004/7BD6E4B8763FE70DB6ADB22B62504361D3778309/ ImageURL: http://cloud-3.steamusercontent.com/ugc/1767069252728651946/04A700179A71859B828E30D2877D802749B8223C/ WidthScale: 0 -Description: See Notebook for details. +Description: After you select 'Enable' from this object's context menu, it will remove + tokens that get moved over it. DragSelectable: true GMNotes: '' GUID: 0a5a29 diff --git a/unpacked/Custom_Tile Token Remover 2ba7a5.ttslua b/unpacked/Custom_Tile Token Remover 2ba7a5.ttslua index b670f04eb..a2b59aa32 100644 --- a/unpacked/Custom_Tile Token Remover 2ba7a5.ttslua +++ b/unpacked/Custom_Tile Token Remover 2ba7a5.ttslua @@ -45,11 +45,11 @@ __bundle_register("__root", function(require, _LOADED, __bundle_register, __bund require("util/TokenRemover") end) __bundle_register("util/TokenRemover", function(require, _LOADED, __bundle_register, __bundle_modules) -local zone = nil +local zone -- general code function onSave() - return JSON.encode(zone and zone.getGUID() or nil) + return JSON.encode(zone and zone.getGUID()) end function onLoad(savedData) diff --git a/unpacked/Custom_Tile Token Remover 2ba7a5.yaml b/unpacked/Custom_Tile Token Remover 2ba7a5.yaml index 7f9df83e6..938675e22 100644 --- a/unpacked/Custom_Tile Token Remover 2ba7a5.yaml +++ b/unpacked/Custom_Tile Token Remover 2ba7a5.yaml @@ -4,9 +4,9 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - b: 1 - g: 1 - r: 1 + b: 0 + g: 0 + r: 0 CustomImage: CustomTile: Stackable: false diff --git a/unpacked/Custom_Tile Token Remover 39b175.ttslua b/unpacked/Custom_Tile Token Remover 39b175.ttslua index b670f04eb..a2b59aa32 100644 --- a/unpacked/Custom_Tile Token Remover 39b175.ttslua +++ b/unpacked/Custom_Tile Token Remover 39b175.ttslua @@ -45,11 +45,11 @@ __bundle_register("__root", function(require, _LOADED, __bundle_register, __bund require("util/TokenRemover") end) __bundle_register("util/TokenRemover", function(require, _LOADED, __bundle_register, __bundle_modules) -local zone = nil +local zone -- general code function onSave() - return JSON.encode(zone and zone.getGUID() or nil) + return JSON.encode(zone and zone.getGUID()) end function onLoad(savedData) diff --git a/unpacked/Custom_Tile Token Remover 39b175.yaml b/unpacked/Custom_Tile Token Remover 39b175.yaml index 432a7968f..e4755fe25 100644 --- a/unpacked/Custom_Tile Token Remover 39b175.yaml +++ b/unpacked/Custom_Tile Token Remover 39b175.yaml @@ -4,9 +4,9 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - b: 1 - g: 1 - r: 1 + b: 0 + g: 0 + r: 0 CustomImage: CustomTile: Stackable: false diff --git a/unpacked/Custom_Tile Token Spawning Reference f8b3a7.yaml b/unpacked/Custom_Tile Token Spawning Reference f8b3a7.yaml index 5dcc800f6..16b0ddc9e 100644 --- a/unpacked/Custom_Tile Token Spawning Reference f8b3a7.yaml +++ b/unpacked/Custom_Tile Token Spawning Reference f8b3a7.yaml @@ -15,7 +15,7 @@ CustomImage: Type: 3 ImageScalar: 1 ImageSecondaryURL: '' - ImageURL: http://cloud-3.steamusercontent.com/ugc/2172484009093238162/ACF3BBD93CB517B0BD0952E9BB78A2D35A62F377/ + ImageURL: http://cloud-3.steamusercontent.com/ugc/2467486908540932616/8370C75D2789E1332836D8C2A31D32542153DE85/ WidthScale: 0 Description: Press a numpad key to spawn the indicated token. DragSelectable: true @@ -41,7 +41,7 @@ Tags: - displacement_excluded Tooltip: true Transform: - posX: -48 + posX: -66 posY: 1.48 posZ: 55 rotX: 0 diff --git a/unpacked/Custom_Token BlessCurse Manager 5933fb.ttslua b/unpacked/Custom_Token BlessCurse Manager 5933fb.ttslua index 9f287be17..3f278ac7f 100644 --- a/unpacked/Custom_Token BlessCurse Manager 5933fb.ttslua +++ b/unpacked/Custom_Token BlessCurse Manager 5933fb.ttslua @@ -44,40 +44,6 @@ end)(nil) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("chaosbag/BlessCurseManager") end) -__bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local TokenArrangerApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") - - -- local function to call the token arranger, if it is on the table - ---@param functionName string Name of the function to cal - ---@param argument? table Parameter to pass - local function callIfExistent(functionName, argument) - local tokenArranger = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenArranger") - if tokenArranger ~= nil then - tokenArranger.call(functionName, argument) - end - end - - -- updates the token modifiers with the provided data - ---@param fullData table Contains the chaos token metadata - TokenArrangerApi.onTokenDataChanged = function(fullData) - callIfExistent("onTokenDataChanged", fullData) - end - - -- deletes already laid out tokens - TokenArrangerApi.deleteCopiedTokens = function() - callIfExistent("deleteCopiedTokens") - end - - -- updates the laid out tokens - TokenArrangerApi.layout = function() - Wait.time(function() callIfExistent("layout") end, 0.1) - end - - return TokenArrangerApi -end -end) __bundle_register("chaosbag/BlessCurseManager", function(require, _LOADED, __bundle_register, __bundle_modules) local chaosBagApi = require("chaosbag/ChaosBagApi") local tokenArrangerApi = require("accessories/TokenArrangerApi") @@ -317,7 +283,10 @@ function releasedToken(param) break end end - updateDisplayAndBroadcast(param.type) + + if not param.fromBag then + updateDisplayAndBroadcast(param.type) + end end -- removes a token (called by cards that seal bless/curse tokens) @@ -344,6 +313,19 @@ function updateDisplayAndBroadcast(type) end end +function getBlessCurseInBag() + local numInBag = { Bless = 0, Curse = 0 } + local chaosBag = chaosBagApi.findChaosBag() + + for _, v in ipairs(chaosBag.getObjects()) do + if v.name == "Bless" or v.name == "Curse" then + numInBag[v.name] = numInBag[v.name] + 1 + end + end + + return numInBag +end + --------------------------------------------------------- -- main functions: add and remove --------------------------------------------------------- @@ -485,6 +467,40 @@ end function none() end end) +__bundle_register("accessories/TokenArrangerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local TokenArrangerApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + -- local function to call the token arranger, if it is on the table + ---@param functionName string Name of the function to cal + ---@param argument? table Parameter to pass + local function callIfExistent(functionName, argument) + local tokenArranger = guidReferenceApi.getObjectByOwnerAndType("Mythos", "TokenArranger") + if tokenArranger ~= nil then + tokenArranger.call(functionName, argument) + end + end + + -- updates the token modifiers with the provided data + ---@param fullData table Contains the chaos token metadata + TokenArrangerApi.onTokenDataChanged = function(fullData) + callIfExistent("onTokenDataChanged", fullData) + end + + -- deletes already laid out tokens + TokenArrangerApi.deleteCopiedTokens = function() + callIfExistent("deleteCopiedTokens") + end + + -- updates the laid out tokens + TokenArrangerApi.layout = function() + Wait.time(function() callIfExistent("layout") end, 0.1) + end + + return TokenArrangerApi +end +end) __bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local ChaosBagApi = {} @@ -492,7 +508,7 @@ do -- respawns the chaos bag with a new state of tokens ---@param tokenList table List of chaos token ids ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) + Global.call("setChaosBagState", tokenList) end -- returns a Table List of chaos token ids in the current chaos bag @@ -519,48 +535,57 @@ do -- returns all sealed tokens on cards to the chaos bag ---@param playerColor string Color of the player to show the broadcast to ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) + Global.call("releaseAllSealedTokens", playerColor) end -- returns all drawn tokens to the chaos bag ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") + Global.call("returnChaosTokens") end -- removes the specified chaos token from the chaos bag ---@param id string ID of the chaos token ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) + Global.call("removeChaosToken", id) end -- returns a chaos token to the bag and calls all relevant functions ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) end -- spawns the specified chaos token and puts it into the chaos bag ---@param id string ID of the chaos token ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) + Global.call("spawnChaosToken", id) end -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the -- contents of the bag should check this method before doing so. -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. + ---@return any: True if the bag is manipulated, false if it should be blocked. ChaosBagApi.canTouchChaosTokens = function() return Global.call("canTouchChaosTokens") end - -- called by playermats (by the "Draw chaos token" button) + -- draws a chaos token to a playermat ---@param mat tts__Object Playermat that triggered this ---@param drawAdditional boolean Controls whether additional tokens should be drawn ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) end -- returns a Table List of chaos token ids in the current chaos bag @@ -580,6 +605,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -587,21 +613,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -613,6 +639,13 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) diff --git a/unpacked/Custom_Token Chaos Bag Stat Tracker 766620.yaml b/unpacked/Custom_Token Chaos Bag Stat Tracker 766620.yaml index cf40b8633..4ce3aa956 100644 --- a/unpacked/Custom_Token Chaos Bag Stat Tracker 766620.yaml +++ b/unpacked/Custom_Token Chaos Bag Stat Tracker 766620.yaml @@ -17,10 +17,7 @@ CustomImage: ImageSecondaryURL: '' ImageURL: https://i.imgur.com/SBE8GR5.png WidthScale: 0 -Description: 'Only tracks tokens that actually hit the playmat. - - - All credit goes to TadGH!' +Description: '' DragSelectable: true GMNotes: '' GUID: '766620' diff --git a/unpacked/Custom_Token Clues 3f22e5.ttslua b/unpacked/Custom_Token Clues 3f22e5.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Clues 3f22e5.ttslua +++ b/unpacked/Custom_Token Clues 3f22e5.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Clues 4111de.ttslua b/unpacked/Custom_Token Clues 4111de.ttslua index 3e8cdcc2f..7f418bf5b 100644 --- a/unpacked/Custom_Token Clues 4111de.ttslua +++ b/unpacked/Custom_Token Clues 4111de.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/GenericCounter") -end) __bundle_register("core/GenericCounter", function(require, _LOADED, __bundle_register, __bundle_modules) MIN_VALUE = 0 MAX_VALUE = 99 @@ -52,21 +49,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +78,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) @@ -100,4 +97,7 @@ function addOrSubtract(_, _, isRightClick) self.editButton({ index = 0, label = tostring(val) }) end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/GenericCounter") +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Clues 891403.ttslua b/unpacked/Custom_Token Clues 891403.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Clues 891403.ttslua +++ b/unpacked/Custom_Token Clues 891403.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Clues db85d6.ttslua b/unpacked/Custom_Token Clues db85d6.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Clues db85d6.ttslua +++ b/unpacked/Custom_Token Clues db85d6.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Damage 1f5a0a.ttslua b/unpacked/Custom_Token Damage 1f5a0a.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Damage 1f5a0a.ttslua +++ b/unpacked/Custom_Token Damage 1f5a0a.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Damage 591a45.ttslua b/unpacked/Custom_Token Damage 591a45.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Damage 591a45.ttslua +++ b/unpacked/Custom_Token Damage 591a45.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Damage e64eec.ttslua b/unpacked/Custom_Token Damage e64eec.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Damage e64eec.ttslua +++ b/unpacked/Custom_Token Damage e64eec.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Damage eb08d6.ttslua b/unpacked/Custom_Token Damage eb08d6.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Damage eb08d6.ttslua +++ b/unpacked/Custom_Token Damage eb08d6.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Doom Counter 85c4c6.ttslua b/unpacked/Custom_Token Doom Counter 85c4c6.ttslua index 1f272e095..35748139d 100644 --- a/unpacked/Custom_Token Doom Counter 85c4c6.ttslua +++ b/unpacked/Custom_Token Doom Counter 85c4c6.ttslua @@ -62,7 +62,7 @@ val = 0 function onSave() return JSON.encode({ val, options }) end function onLoad(savedData) - if savedData ~= "" then + if savedData and savedData ~= "" then local loadedData = JSON.decode(savedData) val = loadedData[1] options = loadedData[2] @@ -190,6 +190,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -197,21 +198,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -223,6 +224,13 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) @@ -239,13 +247,13 @@ do return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end - -- Returns the current value of the investigator counter from the playmat + -- Returns the current value of the investigator counter from the playermat ---@return number: Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getInvestigatorCounter().getVar("val") end - -- Updates the current value of the investigator counter from the playmat + -- Updates the current value of the investigator counter from the playermat ---@param count number Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) getInvestigatorCounter().call("updateVal", count) @@ -286,7 +294,7 @@ do getPlayArea().call("onScenarioChanged", scenarioName) end - -- Sets this playmat's snap points to limit snapping to locations or not. + -- Sets this playermat's snap points to limit snapping to locations or not. -- If matchTypes is false, snap points will be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) @@ -299,18 +307,18 @@ do getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) end - -- counts the VP on locations in the play area + -- Counts the VP on locations in the play area PlayAreaApi.countVP = function() return getPlayArea().call("countVP") end - -- highlights all locations in the play area without metadata + -- Highlights all locations in the play area without metadata ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightMissingData = function(state) return getPlayArea().call("highlightMissingData", state) end - - -- highlights all locations in the play area with VP + + -- Highlights all locations in the play area with VP ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightCountedVP = function(state) return getPlayArea().call("countVP", state) @@ -321,15 +329,26 @@ do return getPlayArea().call("isInPlayArea", object) end + -- Returns the current surface of the play area PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end + -- Updates the surface of the play area PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call PlayAreaApi.updateLocations = function(args) @@ -347,12 +366,12 @@ __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -376,7 +395,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -393,21 +412,22 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end diff --git a/unpacked/Custom_Token Drawing Tool 280086.ttslua b/unpacked/Custom_Token Drawing Tool 280086.ttslua index e1b91b9f2..f2e8beebe 100644 --- a/unpacked/Custom_Token Drawing Tool 280086.ttslua +++ b/unpacked/Custom_Token Drawing Tool 280086.ttslua @@ -45,90 +45,103 @@ __bundle_register("__root", function(require, _LOADED, __bundle_register, __bund require("util/ConnectionDrawingTool") end) __bundle_register("util/ConnectionDrawingTool", function(require, _LOADED, __bundle_register, __bundle_modules) -local lines = {} +local connections = {} --- save "lines" to be able to remove them after loading function onSave() - return JSON.encode(lines) + return JSON.encode({ connections = connections }) end function onLoad(savedData) - lines = JSON.decode(savedData) or {} + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) or {} + connections = loadedData.connections + processLines() + end + + addHotkey("Drawing Tool: Reset", function() connections = {} processLines() end) + addHotkey("Drawing Tool: Redraw", processLines) end --- create timer when numpad 0 is pressed -function onScriptingButtonDown(index, player_color) +function onScriptingButtonDown(index, playerColor) if index ~= 10 then return end - TimerID = Wait.time(function() draw_from(Player[player_color]) end, 1) + + Timer.create { + identifier = playerColor .. "_draw_from", + function_name = "draw_from", + parameters = { player = Player[playerColor] }, + delay = 1 + } end --- called for long press of numpad 0, draws lines from hovered object to selected objects -function draw_from(player) - local source = player.getHoverObject() +function draw_from(params) + local source = params.player.getHoverObject() if not source then return end - for _, item in ipairs(player.getSelectedObjects()) do - if item.getGUID() ~= source.getGUID() then + for _, item in ipairs(params.player.getSelectedObjects()) do + if item ~= source then if item.getGUID() > source.getGUID() then - draw_with_pair(item, source) + addPair(item, source) else - draw_with_pair(source, item) + addPair(source, item) end end end - process_lines() + processLines() end --- general drawing of all lines between selected objects -function onScriptingButtonUp(index, player_color) +function onScriptingButtonUp(index, playerColor) if index ~= 10 then return end - -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second. - if not Wait.stop(TimerID) then return end - local items = Player[player_color].getSelectedObjects() - if #items < 2 then - broadcastToColor("You must have at least two items selected (currently: " .. #items .. ").", player_color, "Red") - return - end + -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second. + if not Timer.destroy(playerColor .. "_draw_from") then return end + + local items = Player[playerColor].getSelectedObjects() + if #items < 2 then return end table.sort(items, function(a, b) return a.getGUID() > b.getGUID() end) - for f = 1, #items - 1 do - for s = f + 1, #items do - draw_with_pair(items[f], items[s]) + for i = 1, #items do + local first = items[i] + + for j = i, #items do + local second = items[j] + addPair(first, second) end end - process_lines() + processLines() end --- adds two objects to table of vector lines -function draw_with_pair(first, second) - local guid_first = first.getGUID() - local guid_second = second.getGUID() +function addPair(first, second) + local first_guid = first.getGUID() + local second_guid = second.getGUID() - if Global.getVectorLines() == nil then lines = {} end - if not lines[guid_first] then lines[guid_first] = {} end - - if lines[guid_first][guid_second] then - lines[guid_first][guid_second] = nil - else - lines[guid_first][guid_second] = { points = { first.getPosition(), second.getPosition() }, color = "White" } - end + if not connections[first_guid] then connections[first_guid] = {} end + connections[first_guid][second_guid] = not connections[first_guid][second_guid] end --- updates the global vector lines based on "lines" -function process_lines() - local drawing = {} +function processLines() + local lines = {} - for _, first in pairs(lines) do - for _, data in pairs(first) do - table.insert(drawing, data) + for source_guid, target_guids in pairs(connections) do + local source = getObjectFromGUID(source_guid) + + for target_guid, exists in pairs(target_guids) do + if exists then + local target = getObjectFromGUID(target_guid) + + if source and target then + table.insert(lines, { + points = { source.getPosition(), target.getPosition() }, + color = Color.White + }) + end + end end end - Global.setVectorLines(drawing) + Global.setVectorLines(lines) end end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Drawing Tool 280086.yaml b/unpacked/Custom_Token Drawing Tool 280086.yaml index ae118facc..5880b6255 100644 --- a/unpacked/Custom_Token Drawing Tool 280086.yaml +++ b/unpacked/Custom_Token Drawing Tool 280086.yaml @@ -35,7 +35,7 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Token Drawing Tool 280086.ttslua' -LuaScriptState: '{"e8e04b":[]}' +LuaScriptState: '{"connections":[]}' MeasureMovement: false Name: Custom_Token Nickname: Drawing Tool diff --git a/unpacked/Custom_Token Horror 0257d9.ttslua b/unpacked/Custom_Token Horror 0257d9.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Horror 0257d9.ttslua +++ b/unpacked/Custom_Token Horror 0257d9.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Horror 468e88.ttslua b/unpacked/Custom_Token Horror 468e88.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Horror 468e88.ttslua +++ b/unpacked/Custom_Token Horror 468e88.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Horror 7b5729.ttslua b/unpacked/Custom_Token Horror 7b5729.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Horror 7b5729.ttslua +++ b/unpacked/Custom_Token Horror 7b5729.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Horror beb964.ttslua b/unpacked/Custom_Token Horror beb964.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Horror beb964.ttslua +++ b/unpacked/Custom_Token Horror beb964.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Instruction Generator 240522.ttslua b/unpacked/Custom_Token Instruction Generator 240522.ttslua index e2e81072b..4cf7ce1bb 100644 --- a/unpacked/Custom_Token Instruction Generator 240522.ttslua +++ b/unpacked/Custom_Token Instruction Generator 240522.ttslua @@ -140,12 +140,12 @@ __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -169,7 +169,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -186,21 +186,22 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end diff --git a/unpacked/Custom_Token Investigator Count f182ee.ttslua b/unpacked/Custom_Token Investigator Count f182ee.ttslua index 6f38634e6..a58999056 100644 --- a/unpacked/Custom_Token Investigator Count f182ee.ttslua +++ b/unpacked/Custom_Token Investigator Count f182ee.ttslua @@ -57,21 +57,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -87,6 +86,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Investigator Skill Tracker af7ed7.ttslua b/unpacked/Custom_Token Investigator Skill Tracker af7ed7.ttslua index e32fa63da..5060842aa 100644 --- a/unpacked/Custom_Token Investigator Skill Tracker af7ed7.ttslua +++ b/unpacked/Custom_Token Investigator Skill Tracker af7ed7.ttslua @@ -58,7 +58,9 @@ function onSave() return JSON.encode(stats) end -- load stats and make buttons (left to right) function onLoad(savedData) - stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + if savedData and savedData ~= "" then + stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + end for index = 1, 4 do local fnName = "buttonClick" .. index diff --git a/unpacked/Custom_Token Investigator Skill Tracker b4a5f7.ttslua b/unpacked/Custom_Token Investigator Skill Tracker b4a5f7.ttslua index e32fa63da..5060842aa 100644 --- a/unpacked/Custom_Token Investigator Skill Tracker b4a5f7.ttslua +++ b/unpacked/Custom_Token Investigator Skill Tracker b4a5f7.ttslua @@ -58,7 +58,9 @@ function onSave() return JSON.encode(stats) end -- load stats and make buttons (left to right) function onLoad(savedData) - stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + if savedData and savedData ~= "" then + stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + end for index = 1, 4 do local fnName = "buttonClick" .. index diff --git a/unpacked/Custom_Token Investigator Skill Tracker e598c2.ttslua b/unpacked/Custom_Token Investigator Skill Tracker e598c2.ttslua index e32fa63da..5060842aa 100644 --- a/unpacked/Custom_Token Investigator Skill Tracker e598c2.ttslua +++ b/unpacked/Custom_Token Investigator Skill Tracker e598c2.ttslua @@ -58,7 +58,9 @@ function onSave() return JSON.encode(stats) end -- load stats and make buttons (left to right) function onLoad(savedData) - stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + if savedData and savedData ~= "" then + stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + end for index = 1, 4 do local fnName = "buttonClick" .. index diff --git a/unpacked/Custom_Token Investigator Skill Tracker e74881.ttslua b/unpacked/Custom_Token Investigator Skill Tracker e74881.ttslua index e32fa63da..5060842aa 100644 --- a/unpacked/Custom_Token Investigator Skill Tracker e74881.ttslua +++ b/unpacked/Custom_Token Investigator Skill Tracker e74881.ttslua @@ -58,7 +58,9 @@ function onSave() return JSON.encode(stats) end -- load stats and make buttons (left to right) function onLoad(savedData) - stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + if savedData and savedData ~= "" then + stats = JSON.decode(savedData) or { 1, 1, 1, 1 } + end for index = 1, 4 do local fnName = "buttonClick" .. index diff --git a/unpacked/Custom_Token Master Clue Counter 4a3aa4.ttslua b/unpacked/Custom_Token Master Clue Counter 4a3aa4.ttslua index 461b56a22..d21752082 100644 --- a/unpacked/Custom_Token Master Clue Counter 4a3aa4.ttslua +++ b/unpacked/Custom_Token Master Clue Counter 4a3aa4.ttslua @@ -44,52 +44,8 @@ end)(nil) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/MasterClueCounter") end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) __bundle_register("core/MasterClueCounter", function(require, _LOADED, __bundle_register, __bundle_modules) -local playmatApi = require("playermat/PlaymatApi") +local playermatApi = require("playermat/PlayermatApi") -- variables are intentionally global to be accessible count = 0 @@ -98,9 +54,10 @@ useClickableCounters = false function onSave() return JSON.encode(useClickableCounters) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then useClickableCounters = JSON.decode(savedData) end + self.createButton({ label = "0", click_function = "removeAllPlayerClues", @@ -114,31 +71,33 @@ function onLoad(savedData) font_color = { 1, 1, 1, 100 }, color = { 0, 0, 0, 0 } }) + Wait.time(sumClues, 2, -1) end -- removes all player clues by calling the respective function from the counting bowls / clickers function removeAllPlayerClues() printToAll(count .. " clue(s) from playermats removed.", "White") - playmatApi.removeClues("All") + playermatApi.removeClues("All") self.editButton({ index = 0, label = "0" }) end -- gets the counted values from the counting bowls / clickers and sums them up function sumClues() - count = playmatApi.getClueCount(useClickableCounters, "All") + count = playermatApi.getClueCount(useClickableCounters, "All") self.editButton({ index = 0, label = tostring(count) }) end end) -__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlaymatApi = {} + local PlayermatApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } -- Convenience function to look up a mat's object by color, or get all mats. - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@return table: Single-element if only single playmat is requested + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested local function getMatForColor(matColor) if matColor == "All" then return guidReferenceApi.getObjectsByType("Playermat") @@ -147,9 +106,9 @@ do end end - -- Returns the color of the closest playmat + -- Returns the color of the closest playermat ---@param startPos table Starting position to get the closest mat from - PlaymatApi.getMatColorByPosition = function(startPos) + PlayermatApi.getMatColorByPosition = function(startPos) local result, smallestDistance for matColor, mat in pairs(getMatForColor("All")) do local distance = Vector.between(startPos, mat.getPosition()):magnitude() @@ -161,17 +120,17 @@ do return result end - -- Returns the color of the player's hand that is seated next to the playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getPlayerColor = function(matColor) + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("playerColor") end end - -- Returns the color of the playmat that owns the playercolor's hand - ---@param handColor string Color of the playmat - PlaymatApi.getMatColor = function(handColor) + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) for matColor, mat in pairs(getMatForColor("All")) do local playerColor = mat.getVar("playerColor") if playerColor == handColor then @@ -180,59 +139,95 @@ do end end - -- Returns if there is the card "Dream-Enhancing Serum" on the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.isDES = function(matColor) + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("isDES") + mat.call("checkForDES") end end - -- Performs a search of the deck area of the requested playmat and returns the result as table - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDeckAreaObjects = function(matColor) + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getDeckAreaObjects") end end -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.flipTopCardFromDeck = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("flipTopCardFromDeck") end end - -- Returns the position of the discard pile of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDiscardPosition = function(matColor) + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("returnGlobalDiscardPosition") end end + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + -- Transforms a local position into a global position ---@param localPos table Local position to be transformed - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.transformLocalPosition = function(localPos, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.positionToWorld(localPos) end end - -- Returns the rotation of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.returnRotation = function(matColor) + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getRotation() end end -- Returns a table with spawn data (position and rotation) for a helper object - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param helperName string Name of the helper object - PlaymatApi.getHelperSpawnData = function(matColor, helperName) + PlayermatApi.getHelperSpawnData = function(matColor, helperName) local resultTable = {} local localPositionTable = { ["Hand Helper"] = {0.05, 0, -1.182}, @@ -249,73 +244,99 @@ do end - -- Triggers the Upkeep for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param playerColor string Color of the calling player (for messages) - PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doUpkeepFromHotkey", playerColor) 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") + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param objList table List of objects to discard - PlaymatApi.discardListOfObjects = function(matColor, objList) + PlayermatApi.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) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("activeInvestigatorId") end end - -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If -- matchTypes is true, the main card slot snap points will only snap assets, while the -- investigator area point will only snap Investigators. If matchTypes is false, snap points will -- be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("setLimitSnapsByType", matchCardTypes) end end - -- Sets the requested playmat's draw 1 button to visible + -- Sets the requested playermat's draw 1 button to visible ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("showDrawButton", isDrawButtonVisible) end end - -- Shows or hides the clickable clue counter for the requested playmat + -- Shows or hides the clickable clue counter for the requested playermat ---@param showCounter boolean Whether the clickable counter should be present or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.clickableClues = function(showCounter, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("clickableClues", showCounter) end end - -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.removeClues = function(matColor) + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("removeClues") end end - -- Reports the clue count for the requested playmat + -- Reports the clue count for the requested playermat ---@param useClickableCounters boolean Controls which type of counter is getting checked - PlaymatApi.getClueCount = function(useClickableCounters, matColor) + PlayermatApi.getClueCount = function(useClickableCounters, matColor) local count = 0 for _, mat in pairs(getMatForColor(matColor)) do count = count + mat.call("getClueCount", useClickableCounters) @@ -323,44 +344,41 @@ do return count end - -- updates the specified owned counter - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param type string Counter to target ---@param newValue number Value to set the counter to ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier - PlaymatApi.updateCounter = function(matColor, type, newValue, modifier) + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) for _, mat in pairs(getMatForColor(matColor)) do mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) end end - -- triggers the draw function for the specified playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param number number Amount of cards to draw - PlaymatApi.drawCardsWithReshuffle = function(matColor, number) + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) for _, mat in pairs(getMatForColor(matColor)) do mat.call("drawCardsWithReshuffle", number) end end - -- returns the resource counter amount - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param type string Counter to target - PlaymatApi.getCounterValue = function(matColor, type) + PlayermatApi.getCounterValue = function(matColor, type) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getCounterValue", type) end end - -- returns a list of mat colors that have an investigator placed - PlaymatApi.getUsedMatColors = function() - local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() local usedColors = {} - for matColor, mat in pairs(getMatForColor("All")) do local searchPos = mat.positionToWorld(localInvestigatorPosition) local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") - if #searchResult > 0 then table.insert(usedColors, matColor) end @@ -368,18 +386,39 @@ do return usedColors end - -- resets the specified skill tracker to "1, 1, 1, 1" - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.resetSkillTracker = function(matColor) + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("resetSkillTracker") end end - -- finds all objects on the playmat and associated set aside zone and returns a table - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param filter string Name of the filte function (see util/SearchLib) - PlaymatApi.searchAroundPlaymat = function(matColor, filter) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} for _, mat in pairs(getMatForColor(matColor)) do for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do @@ -390,33 +429,85 @@ do end -- Discard a non-hidden card from the corresponding player's hand - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.doDiscardOne = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doDiscardOne") end end - -- Triggers the metadata sync for all playmats - PlaymatApi.syncAllCustomizableCards = function() + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() for _, mat in pairs(getMatForColor("All")) do mat.call("syncAllCustomizableCards") end end - return PlaymatApi + return PlayermatApi +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -440,7 +531,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -457,21 +548,22 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end diff --git a/unpacked/Custom_Token Other Doom in Play 652ff3.ttslua b/unpacked/Custom_Token Other Doom in Play 652ff3.ttslua index 3962eba9e..1d135c41d 100644 --- a/unpacked/Custom_Token Other Doom in Play 652ff3.ttslua +++ b/unpacked/Custom_Token Other Doom in Play 652ff3.ttslua @@ -46,12 +46,12 @@ require("core/DoomInPlayCounter") end) __bundle_register("core/DoomInPlayCounter", function(require, _LOADED, __bundle_register, __bundle_modules) local guidReferenceApi = require("core/GUIDReferenceApi") -local playmatApi = require("playermat/PlaymatApi") +local playermatApi = require("playermat/PlayermatApi") local ZONE, TRASH -local doomURL = "https://i.imgur.com/EoL7yaZ.png" -local IGNORE_TAG = "DoomCounter_ignore" -local TOTAL_PLAY_AREA = { +local doomURL = "https://i.imgur.com/EoL7yaZ.png" +local IGNORE_TAG = "DoomCounter_ignore" +local TOTAL_PLAY_AREA = { upperLeft = { x = -9, z = -35 @@ -113,15 +113,15 @@ end -- removes doom from playermats / playarea function removeDoom(options) if options.Playermats then - local count = removeDoomFromList(playmatApi.searchAroundPlaymat("All")) - if count > 0 then + local count = removeDoomFromList(playermatApi.searchAroundPlayermat("All")) + if count > 0 then broadcastToAll(count .. " doom removed from playermats.", "White") end end if options.Playarea then local count = removeDoomFromList(ZONE.getObjects()) - if count > 0 then + if count > 0 then broadcastToAll(count .. " doom removed from play area.", "White") end end @@ -143,9 +143,9 @@ end -- helper function to check if a position is inside an area 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 end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -156,6 +156,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -163,21 +164,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -189,18 +190,26 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) -__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlaymatApi = {} + local PlayermatApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } -- Convenience function to look up a mat's object by color, or get all mats. - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@return table: Single-element if only single playmat is requested + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested local function getMatForColor(matColor) if matColor == "All" then return guidReferenceApi.getObjectsByType("Playermat") @@ -209,9 +218,9 @@ do end end - -- Returns the color of the closest playmat + -- Returns the color of the closest playermat ---@param startPos table Starting position to get the closest mat from - PlaymatApi.getMatColorByPosition = function(startPos) + PlayermatApi.getMatColorByPosition = function(startPos) local result, smallestDistance for matColor, mat in pairs(getMatForColor("All")) do local distance = Vector.between(startPos, mat.getPosition()):magnitude() @@ -223,17 +232,17 @@ do return result end - -- Returns the color of the player's hand that is seated next to the playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getPlayerColor = function(matColor) + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("playerColor") end end - -- Returns the color of the playmat that owns the playercolor's hand - ---@param handColor string Color of the playmat - PlaymatApi.getMatColor = function(handColor) + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) for matColor, mat in pairs(getMatForColor("All")) do local playerColor = mat.getVar("playerColor") if playerColor == handColor then @@ -242,59 +251,95 @@ do end end - -- Returns if there is the card "Dream-Enhancing Serum" on the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.isDES = function(matColor) + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("isDES") + mat.call("checkForDES") end end - -- Performs a search of the deck area of the requested playmat and returns the result as table - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDeckAreaObjects = function(matColor) + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getDeckAreaObjects") end end -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.flipTopCardFromDeck = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("flipTopCardFromDeck") end end - -- Returns the position of the discard pile of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDiscardPosition = function(matColor) + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("returnGlobalDiscardPosition") end end + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + -- Transforms a local position into a global position ---@param localPos table Local position to be transformed - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.transformLocalPosition = function(localPos, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.positionToWorld(localPos) end end - -- Returns the rotation of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.returnRotation = function(matColor) + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getRotation() end end -- Returns a table with spawn data (position and rotation) for a helper object - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param helperName string Name of the helper object - PlaymatApi.getHelperSpawnData = function(matColor, helperName) + PlayermatApi.getHelperSpawnData = function(matColor, helperName) local resultTable = {} local localPositionTable = { ["Hand Helper"] = {0.05, 0, -1.182}, @@ -311,73 +356,99 @@ do end - -- Triggers the Upkeep for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param playerColor string Color of the calling player (for messages) - PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doUpkeepFromHotkey", playerColor) 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") + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param objList table List of objects to discard - PlaymatApi.discardListOfObjects = function(matColor, objList) + PlayermatApi.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) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("activeInvestigatorId") end end - -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If -- matchTypes is true, the main card slot snap points will only snap assets, while the -- investigator area point will only snap Investigators. If matchTypes is false, snap points will -- be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("setLimitSnapsByType", matchCardTypes) end end - -- Sets the requested playmat's draw 1 button to visible + -- Sets the requested playermat's draw 1 button to visible ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("showDrawButton", isDrawButtonVisible) end end - -- Shows or hides the clickable clue counter for the requested playmat + -- Shows or hides the clickable clue counter for the requested playermat ---@param showCounter boolean Whether the clickable counter should be present or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.clickableClues = function(showCounter, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("clickableClues", showCounter) end end - -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.removeClues = function(matColor) + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("removeClues") end end - -- Reports the clue count for the requested playmat + -- Reports the clue count for the requested playermat ---@param useClickableCounters boolean Controls which type of counter is getting checked - PlaymatApi.getClueCount = function(useClickableCounters, matColor) + PlayermatApi.getClueCount = function(useClickableCounters, matColor) local count = 0 for _, mat in pairs(getMatForColor(matColor)) do count = count + mat.call("getClueCount", useClickableCounters) @@ -385,44 +456,41 @@ do return count end - -- updates the specified owned counter - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param type string Counter to target ---@param newValue number Value to set the counter to ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier - PlaymatApi.updateCounter = function(matColor, type, newValue, modifier) + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) for _, mat in pairs(getMatForColor(matColor)) do mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) end end - -- triggers the draw function for the specified playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param number number Amount of cards to draw - PlaymatApi.drawCardsWithReshuffle = function(matColor, number) + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) for _, mat in pairs(getMatForColor(matColor)) do mat.call("drawCardsWithReshuffle", number) end end - -- returns the resource counter amount - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param type string Counter to target - PlaymatApi.getCounterValue = function(matColor, type) + PlayermatApi.getCounterValue = function(matColor, type) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getCounterValue", type) end end - -- returns a list of mat colors that have an investigator placed - PlaymatApi.getUsedMatColors = function() - local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() local usedColors = {} - for matColor, mat in pairs(getMatForColor("All")) do local searchPos = mat.positionToWorld(localInvestigatorPosition) local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") - if #searchResult > 0 then table.insert(usedColors, matColor) end @@ -430,18 +498,39 @@ do return usedColors end - -- resets the specified skill tracker to "1, 1, 1, 1" - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.resetSkillTracker = function(matColor) + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("resetSkillTracker") end end - -- finds all objects on the playmat and associated set aside zone and returns a table - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param filter string Name of the filte function (see util/SearchLib) - PlaymatApi.searchAroundPlaymat = function(matColor, filter) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} for _, mat in pairs(getMatForColor(matColor)) do for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do @@ -452,33 +541,33 @@ do end -- Discard a non-hidden card from the corresponding player's hand - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.doDiscardOne = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doDiscardOne") end end - -- Triggers the metadata sync for all playmats - PlaymatApi.syncAllCustomizableCards = function() + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() for _, mat in pairs(getMatForColor("All")) do mat.call("syncAllCustomizableCards") end end - return PlaymatApi + return PlayermatApi end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -502,7 +591,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -519,21 +608,22 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end diff --git a/unpacked/Custom_Token Play Area 721ba2.ttslua b/unpacked/Custom_Token Play Area 721ba2.ttslua index ff6faa232..a3b0c59e5 100644 --- a/unpacked/Custom_Token Play Area 721ba2.ttslua +++ b/unpacked/Custom_Token Play Area 721ba2.ttslua @@ -41,69 +41,132 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/PlayArea") -end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local GUIDReferenceApi = {} + local PlayAreaApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") - local function getGuidHandler() - return getObjectFromGUID("123456") + local function getPlayArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") end - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + local function getInvestigatorCounter() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) + -- Returns the current value of the investigator counter from the playermat + ---@return number: Number of investigators currently set on the counter + PlayAreaApi.getInvestigatorCount = function() + return getInvestigatorCounter().getVar("val") end - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) + -- Updates the current value of the investigator counter from the playermat + ---@param count number Number of investigators to set on the counter + PlayAreaApi.setInvestigatorCount = function(count) + getInvestigatorCounter().call("updateVal", count) end - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) + -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain + -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' + ---@param playerColor string Color of the player requesting the shift for messages + PlayAreaApi.shiftContentsUp = function(playerColor) + getPlayArea().call("shiftContentsUp", playerColor) end - return GUIDReferenceApi -end -end) -__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local OptionPanelApi = {} - - -- loads saved options - ---@param options table Set a new state for the option table - OptionPanelApi.loadSettings = function(options) - return Global.call("loadSettings", options) + PlayAreaApi.shiftContentsDown = function(playerColor) + getPlayArea().call("shiftContentsDown", playerColor) end - ---@return any: Table of option panel state - OptionPanelApi.getOptions = function() - return Global.getTable("optionPanel") + PlayAreaApi.shiftContentsLeft = function(playerColor) + getPlayArea().call("shiftContentsLeft", playerColor) end - return OptionPanelApi + PlayAreaApi.shiftContentsRight = function(playerColor) + getPlayArea().call("shiftContentsRight", playerColor) + end + + ---@param state boolean This controls whether location connections should be drawn + PlayAreaApi.setConnectionDrawState = function(state) + getPlayArea().call("setConnectionDrawState", state) + end + + ---@param color string Connection color to be used for location connections + PlayAreaApi.setConnectionColor = function(color) + getPlayArea().call("setConnectionColor", color) + end + + -- Event to be called when the current scenario has changed + ---@param scenarioName string Name of the new scenario + PlayAreaApi.onScenarioChanged = function(scenarioName) + getPlayArea().call("onScenarioChanged", scenarioName) + end + + -- Sets this playermat's snap points to limit snapping to locations or not. + -- If matchTypes is false, snap points will be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) + getPlayArea().call("setLimitSnapsByType", matchCardTypes) + end + + -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged + -- cards before they're destroyed by entering the container + PlayAreaApi.tryObjectEnterContainer = function(container, object) + getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) + end + + -- Counts the VP on locations in the play area + PlayAreaApi.countVP = function() + return getPlayArea().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 getPlayArea().call("highlightMissingData", state) + end + + -- Highlights all locations in the play area with VP + ---@param state boolean True if highlighting should be enabled + PlayAreaApi.highlightCountedVP = function(state) + return getPlayArea().call("countVP", state) + end + + -- Checks if an object is in the play area (returns true or false) + PlayAreaApi.isInPlayArea = function(object) + return getPlayArea().call("isInPlayArea", object) + end + + -- Returns the current surface of the play area + PlayAreaApi.getSurface = function() + return getPlayArea().getCustomObject().image + end + + -- Updates the surface of the play area + PlayAreaApi.updateSurface = function(url) + return getPlayArea().call("updateSurface", url) + end + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + -- data to the local token manager instance. + ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call + PlayAreaApi.updateLocations = function(args) + getPlayArea().call("updateLocations", args) + end + + PlayAreaApi.getCustomDataHelper = function() + return getPlayArea().getVar("customDataHelper") + end + + return PlayAreaApi end end) __bundle_register("core/PlayArea", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -169,17 +232,21 @@ function onSave() end function onLoad(savedData) - self.interactable = false -- this needs to be here since the playarea will be reloaded when the image changes - local loadedData = JSON.decode(savedData) or {} - locations = loadedData.trackedLocations or {} - currentScenario = loadedData.currentScenario - connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 } - connectionsEnabled = loadedData.connectionsEnabled or true + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) or {} + locations = loadedData.trackedLocations or {} + currentScenario = loadedData.currentScenario + connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 } + connectionsEnabled = loadedData.connectionsEnabled + end + + -- this needs to be here since the playarea will be reloaded when the image changes + self.interactable = false Wait.time(function() collisionEnabled = true end, 0.1) end --- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the +-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call function updateLocations(args) @@ -189,31 +256,10 @@ function updateLocations(args) end end --- sets the image of the playarea -function updateSurface(newURL) - local customInfo = self.getCustomObject() +--------------------------------------------------------- +-- TTS event handling +--------------------------------------------------------- - if newURL ~= "" and newURL ~= nil and newURL ~= DEFAULT_URL then - customInfo.image = newURL - broadcastToAll("New Playarea Image Applied", "Green") - else - customInfo.image = DEFAULT_URL - broadcastToAll("Default Playarea Image Applied", "Green") - end - - self.setCustomObject(customInfo) - - local guid = nil - - if customDataHelper then guid = customDataHelper.getGUID() end - self.reload() - - if guid ~= nil then - Wait.time(function() updateLocations({ guid }) end, 1) - end -end - --- TTS event, called for each object that is placed on the playarea function onCollisionEnter(collisionInfo) if not collisionEnabled then return end @@ -231,7 +277,7 @@ function onCollisionEnter(collisionInfo) tokenManager.spawnForCard(object) end - -- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send + -- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send -- the dropped card immediately into a deck, so this has to be done here if draggingGuids[object.getGUID()] ~= nil then object.setVectorLines({}) @@ -241,6 +287,80 @@ function onCollisionEnter(collisionInfo) maybeTrackLocation(object) end +function onCollisionExit(collisionInfo) + maybeUntrackLocation(collisionInfo.collision_object) +end + +-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well +function onObjectDestroy(object) + maybeUntrackLocation(object) +end + +function onObjectPickUp(_, object) + if object.type ~= "Card" then return end + + -- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we should be tracking + 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 + -- 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() + if object.is_face_down then + draggingGuids[pickedUpGuid] = metadata.locationBack + else + draggingGuids[pickedUpGuid] = metadata.locationFront + end + rebuildConnectionList() + end, 2) + end + end +end + +-- Due to the frequence of onUpdate calls, ensure that we only process any changes once +function onUpdate() + local needsConnectionRebuild = false + local needsConnectionDraw = false + for guid, _ in pairs(draggingGuids) do + local obj = getObjectFromGUID(guid) + if obj == nil or not isInPlayArea(obj) then + draggingGuids[guid] = nil + needsConnectionRebuild = true + -- If object still exists then it's outside the area and needs to lose the lines attached to it + if obj ~= nil then + obj.setVectorLines({}) + end + end + -- Even if the last location left the play area, need one last draw to clear the lines + needsConnectionDraw = true + end + if needsConnectionRebuild then + rebuildConnectionList() + end + if needsConnectionDraw then + drawDraggingConnections() + end +end + +-- Global event handler, delegated from Global. Clears any connection lines from dragged cards +-- before they are destroyed by entering a deck. Removal of the card from the dragging list will +-- be handled during the next onUpdate() call. +function tryObjectEnterContainer() + for draggedGuid, _ in pairs(draggingGuids) do + local draggedObj = getObjectFromGUID(draggedGuid) + if draggedObj ~= nil then + draggedObj.setVectorLines({}) + end + end +end + +--------------------------------------------------------- +-- main functionality +--------------------------------------------------------- + function shouldSpawnTokens(card) local metadata = JSON.decode(card.getGMNotes()) if metadata == nil then @@ -257,69 +377,6 @@ function shouldSpawnTokens(card) or metadata.id == "09100" end -function onCollisionExit(collisionInfo) - maybeUntrackLocation(collisionInfo.collision_object) -end - --- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well -function onObjectDestroy(object) - maybeUntrackLocation(object) -end - -function onObjectPickUp(player, object) - -- only continue for cards - local objType = object.name - if objType ~= "Card" and objType ~= "CardCustom" then return end - - -- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we - -- should be tracking - 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 - -- 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() - if object.is_face_down then - draggingGuids[pickedUpGuid] = metadata.locationBack - else - draggingGuids[pickedUpGuid] = metadata.locationFront - end - rebuildConnectionList() - end, 2) - end - end -end - -function onUpdate() - -- Due to the frequence of onUpdate calls, ensure that we only process any changes to the - -- connection list once, and only redraw once - local needsConnectionRebuild = false - local needsConnectionDraw = false - for guid, _ in pairs(draggingGuids) do - local obj = getObjectFromGUID(guid) - if obj == nil or not isInPlayArea(obj) then - draggingGuids[guid] = nil - needsConnectionRebuild = true - -- If object still exists then it's been dragged outside the area and needs to clear the - -- lines attached to it - if obj ~= nil then - obj.setVectorLines({}) - end - end - -- Even if the last location left the play area, need one last draw to clear the lines - needsConnectionDraw = true - end - if needsConnectionRebuild then - rebuildConnectionList() - end - if needsConnectionDraw then - drawDraggingConnections() - end -end - -- Checks the given card and adds it to the list of locations tracked for connection purposes. -- A card will be added to the tracking if it is a location in the play area (based on centerpoint). ---@param card tts__Object A card object, possibly a location. @@ -348,14 +405,14 @@ function maybeTrackLocation(card) end end --- 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 +-- 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 -- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will -- be cleared in the next onUpdate() cycle. ---@param card tts__Object Card to (maybe) stop tracking function maybeUntrackLocation(card) -- Locked objects no longer collide (hence triggering an exit event) but are still in the play - -- area. If the object is now locked, don't remove it. + -- area. If the object is now locked, don't remove it. if locations[card.getGUID()] ~= nil and not card.locked then locations[card.getGUID()] = nil rebuildConnectionList() @@ -363,22 +420,9 @@ function maybeUntrackLocation(card) end end --- Global event handler, delegated from Global. Clears any connection lines from dragged cards --- before they are destroyed by entering a deck. Removal of the card from the dragging list will --- be handled during the next onUpdate() call. -function tryObjectEnterContainer() - for draggedGuid, _ in pairs(draggingGuids) do - local draggedObj = getObjectFromGUID(draggedGuid) - if draggedObj ~= nil then - draggedObj.setVectorLines({}) - end - end -end - -- Builds a list of GUID to GUID connection information based on the currently tracked locations. -- This will update the connection information and store it in the locationConnections data member, --- but does not draw those connections. This should often be followed by a call to --- drawBaseConnections() +-- but does not draw those connections. This should often be followed by a call to drawBaseConnections() function rebuildConnectionList() if not showLocationLinks() then locationConnections = {} @@ -409,7 +453,7 @@ 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 +---@param iconCardList table A table of icon->GUID list. Mutable, will be updated by this method ---@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 @@ -425,7 +469,7 @@ end -- Builds the connections for the given cardID by finding matching icons and adding them to the -- 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. +---@param iconCardList table A table of icon->GUID List. Used to find matching icons for connections. ---@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 @@ -436,7 +480,7 @@ function buildConnection(cardId, iconCardList, locData) -- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way if locationConnections[connectedGuid] ~= nil and (locationConnections[connectedGuid][cardId] == ONE_WAY - or locationConnections[connectedGuid][cardId] == BIDIRECTIONAL) then + or locationConnections[connectedGuid][cardId] == BIDIRECTIONAL) then locationConnections[connectedGuid][cardId] = BIDIRECTIONAL locationConnections[cardId][connectedGuid] = nil else @@ -453,7 +497,6 @@ function buildConnection(cardId, iconCardList, locData) end -- Draws the lines for connections currently in locationConnections but not in draggingGuids. --- Constructed vectors will be set to the playmat function drawBaseConnections() if not showLocationLinks() then locationConnections = {} @@ -486,10 +529,7 @@ end -- Draws the lines for cards which are currently being dragged. function drawDraggingConnections() - if not showLocationLinks() then - return - end - local cardConnectionLines = {} + if not showLocationLinks() then return end local ownedVectors = {} for originGuid, _ in pairs(draggingGuids) do @@ -519,13 +559,12 @@ function drawDraggingConnections() end end --- Draws a bidirectional location connection between the two cards, adding the lines to do so to the --- given lines list. +-- Draws a bidirectional location connection between the two cards, adding the necessary lines to the list ---@param card1 tts__Object One of the card objects to connect ---@param card2 tts__Object The other card object to connect ---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative --- positioning and scaling, as well as highlighting connections during a drag operation ----@param lines table List of vector line elements. Mutable, will be updated to add this connector +---@param lines table List of vector line elements. Mutable, will be updated to add this connector function addBidirectionalVector(card1, card2, vectorOwner, lines) local cardPos1 = card1.getPosition() local cardPos2 = card2.getPosition() @@ -543,12 +582,12 @@ function addBidirectionalVector(card1, card2, vectorOwner, lines) end -- Draws a one-way location connection between the two cards, adding the lines to do so to the --- given lines list. Arrows will point towards the target card. +-- given lines list. Arrows will point towards the target card. ---@param origin tts__Object Origin card in the connection ---@param target tts__Object Target card object to connect ---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative --- positioning and scaling, as well as highlighting connections during a drag operation ----@param lines table List of vector line elements. Mutable, will be updated to add this connector +---@param lines table List of vector line elements. Mutable, will be updated to add this connector function addOneWayVector(origin, target, vectorOwner, lines) -- Start with the BiDi then add the arrow lines to it addBidirectionalVector(origin, target, vectorOwner, lines) @@ -557,12 +596,11 @@ function addOneWayVector(origin, target, vectorOwner, lines) originPos.y = CONNECTION_LINE_Y targetPos.y = CONNECTION_LINE_Y - -- Calculate card distance to be closer for horizontal positions than vertical, since cards are - -- taller than they are wide + -- Calculate distance to be closer for horizontal positions than vertical, since cards are taller than wide local heading = Vector(originPos):sub(targetPos):heading("y") local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading))) - -- Calculate the three possible arrow positions. These are offset by half the arrow length to + -- Calculate the three possible arrow positions. These are offset by half the arrow length to -- make them visually balanced by keeping the arrows centered, not tracking the point local midpoint = Vector(originPos):add(targetPos):scale(0.5):moveTowards(targetPos, ARROW_ARM_LENGTH / 2) local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2) @@ -579,9 +617,9 @@ end -- Draws an arrowhead at the given position. ---@param arrowheadPos tts__Vector Centerpoint of the arrowhead to draw (NOT the tip of the arrow) ---@param originPos tts__Vector Origin point of the connection, used to position the arrow arms ----@param vectorOwner tts__Object The object which these lines will be set to. Used for relative +---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative --- positioning and scaling, as well as highlighting connections during a drag operation ----@param lines table List of vector line elements. Mutable, will be updated to add this arrow +---@param lines table List of vector line elements. Mutable, will be updated to add this arrow function addArrowLines(arrowheadPos, originPos, vectorOwner, lines) local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", -1 * ARROW_ANGLE):add(arrowheadPos) local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", ARROW_ANGLE):add(arrowheadPos) @@ -596,10 +634,63 @@ function addArrowLines(arrowheadPos, originPos, vectorOwner, lines) }) end --- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain +-- Count victory points from locations in play area +---@param highlightOff boolean True if highlighting should be enabled +---@return. Returns the total amount of VP found in the play area +function countVP(highlightOff) + local totalVP = 0 + + for cardId, metadata in pairs(locations) do + local card = getObjectFromGUID(cardId) + if metadata ~= nil and card ~= nil then + if highlightOff == true then + card.highlightOff("Green") + end + + local cardVP = tonumber(metadata.victory) or 0 + if cardVP ~= 0 and not cardHasClues(card) then + totalVP = totalVP + cardVP + if highlightOff == false then + card.highlightOn("Green") + end + end + end + end + + return totalVP +end + +-- Checks if a card has clues on it +---@param card tts__Object Card to check for clues +---@return boolean hasClues True if card has clues on it +function cardHasClues(card) + local searchResult = searchLib.onObject(card, "isClue") + return #searchResult > 0 +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 + +--------------------------------------------------------- +-- functions for outside calls +--------------------------------------------------------- + +-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' ----@param playerColor string Color of the player requesting the shift. Used solely to send an error ---- message in the unlikely case that the scripting zone has been deleted +---@param playerColor string Color of the player requesting the shift (used for error messages) function shiftContentsUp(playerColor) shiftContents(playerColor, "up") end @@ -631,46 +722,48 @@ function shiftContents(playerColor, direction) Wait.time(drawBaseConnections, 0.1) end --- Check to see if the given object is within the bounds of the play area, based solely on the X and --- Z coordinates, ignoring height ----@param object tts__Object Object to check ----@return boolean: True if the object is inside the play area -function isInPlayArea(object) - local bounds = self.getBounds() - local position = object.getPosition() - -- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up - local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2 } - local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2 } +-- Sets the image of the playarea +---@param newURL string URL for the new surface image +function updateSurface(newURL) + local customInfo = self.getCustomObject() - return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z -end + if newURL ~= "" and newURL ~= nil and newURL ~= DEFAULT_URL then + customInfo.image = newURL + broadcastToAll("New Playarea Image Applied", "Green") + else + customInfo.image = DEFAULT_URL + broadcastToAll("Default Playarea Image Applied", "Green") + end -function onScenarioChanged(scenarioName) - currentScenario = scenarioName - if not showLocationLinks() then - broadcastToAll("Automatic location connections not available for this scenario") + self.setCustomObject(customInfo) + + local guid = nil + if customDataHelper then + guid = customDataHelper.getGUID() + end + + self.script_state = onSave() + self.reload() + + if guid ~= nil then + Wait.time(function() updateLocations({ guid }) end, 1) end end -function showLocationLinks() - return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario] and connectionsEnabled -end - --- Sets this playmat's snap points to limit snapping to locations or not. --- If matchTypes is false, snap points will be reset to snap all cards. ----@param matchTypes boolean Whether snap points should only snap for the matching card types. +-- Toggles the tags for the playarea's snap points to limit snapping to locations or not +-- If matchTypes is false, snap points will be reset to snap all cards +---@param matchTypes boolean Whether snap points should only snap for the matching card types function setLimitSnapsByType(matchTypes) local snaps = self.getSnapPoints() - for i, snap in ipairs(snaps) do - local snapTags = snaps[i].tags + for _, snap in ipairs(snaps) do if matchTypes then - if snapTags == nil then - snaps[i].tags = { "Location" } + if snap.tags == nil then + snap.tags = { "Location" } else - table.insert(snaps[i].tags, "Location") + table.insert(snap.tags, "Location") end else - snaps[i].tags = nil + snap.tags = nil end end self.setSnapPoints(snaps) @@ -690,56 +783,44 @@ function setConnectionColor(color) drawBaseConnections() end --- count victory points on locations in play area ----@param highlightOff boolean True if highlighting should be enabled ----@return. Returns the total amount of VP found in the play area -function countVP(highlightOff) - local totalVP = 0 - - for cardId, metadata in pairs(locations) do - local card = getObjectFromGUID(cardId) - if metadata ~= nil and card ~= nil then - if highlightOff == true then - card.highlightOff("Green") - end - - local cardVP = tonumber(metadata.victory) or 0 - if cardVP ~= 0 and not cardHasClues(card) then - totalVP = totalVP + cardVP - if highlightOff == false then - card.highlightOn("Green") - end - end - end - end - - return totalVP -end - --- checks if a card has clues on it, returns true if clues are on it ----@param card tts__Object Card to check for clues -function cardHasClues(card) - local searchResult = searchLib.onObject(card, "isClue") - return #searchResult > 0 -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 +function onScenarioChanged(scenarioName) + currentScenario = scenarioName + if not showLocationLinks() then + broadcastToAll("Automatic location connections not available for this scenario") end end --- rebuilds local snap points (could be useful in the future again) +function getTrackedLocations() + return locations +end + +--------------------------------------------------------- +-- utility functions +--------------------------------------------------------- + +-- Check to see if the given object is within the bounds of the play area (using X and Z coordinates) +---@param object tts__Object Object to check +---@return boolean: True if the object is inside the play area +function isInPlayArea(object) + local bounds = self.getBounds() + local position = object.getPosition() + -- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up + local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2 } + local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2 } + + return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z +end + +function showLocationLinks() + return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario] and connectionsEnabled +end + +function round(num, numDecimalPlaces) + local mult = 10 ^ (numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +-- Rebuilds local snap points (could be useful in the future again) function buildSnaps() local upperleft = { x = 1.53, z = -1.09 } local lowerright = { x = -1.53, z = 1.55 } @@ -765,128 +846,77 @@ function buildSnaps() end self.setSnapPoints(snaps) end - --- utility function -function round(num, numDecimalPlaces) - local mult = 10 ^ (numDecimalPlaces or 0) - return math.floor(num * mult + 0.5) / mult -end end) -__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlayAreaApi = {} - local guidReferenceApi = require("core/GUIDReferenceApi") + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } - local function getPlayArea() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlayArea") + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList end - local function getInvestigatorCounter() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) end - -- Returns the current value of the investigator counter from the playmat - ---@return number: Number of investigators currently set on the counter - PlayAreaApi.getInvestigatorCount = function() - return getInvestigatorCounter().getVar("val") + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) end - -- Updates the current value of the investigator counter from the playmat - ---@param count number Number of investigators to set on the counter - PlayAreaApi.setInvestigatorCount = function(count) - getInvestigatorCounter().call("updateVal", count) + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) end - -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain - -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded' - ---@param playerColor string Color of the player requesting the shift for messages - PlayAreaApi.shiftContentsUp = function(playerColor) - getPlayArea().call("shiftContentsUp", playerColor) + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) end - PlayAreaApi.shiftContentsDown = function(playerColor) - getPlayArea().call("shiftContentsDown", playerColor) - end - - PlayAreaApi.shiftContentsLeft = function(playerColor) - getPlayArea().call("shiftContentsLeft", playerColor) - end - - PlayAreaApi.shiftContentsRight = function(playerColor) - getPlayArea().call("shiftContentsRight", playerColor) - end - - ---@param state boolean This controls whether location connections should be drawn - PlayAreaApi.setConnectionDrawState = function(state) - getPlayArea().call("setConnectionDrawState", state) - end - - ---@param color string Connection color to be used for location connections - PlayAreaApi.setConnectionColor = function(color) - getPlayArea().call("setConnectionColor", color) - end - - -- Event to be called when the current scenario has changed - ---@param scenarioName string Name of the new scenario - PlayAreaApi.onScenarioChanged = function(scenarioName) - getPlayArea().call("onScenarioChanged", scenarioName) - end - - -- Sets this playmat's snap points to limit snapping to locations or not. - -- If matchTypes is false, snap points will be reset to snap all cards. - ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) - getPlayArea().call("setLimitSnapsByType", matchCardTypes) - end - - -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged - -- cards before they're destroyed by entering the container - PlayAreaApi.tryObjectEnterContainer = function(container, object) - getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) - end - - -- counts the VP on locations in the play area - PlayAreaApi.countVP = function() - return getPlayArea().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 getPlayArea().call("highlightMissingData", state) - end - - -- highlights all locations in the play area with VP - ---@param state boolean True if highlighting should be enabled - PlayAreaApi.highlightCountedVP = function(state) - return getPlayArea().call("countVP", state) - end - - -- Checks if an object is in the play area (returns true or false) - PlayAreaApi.isInPlayArea = function(object) - return getPlayArea().call("isInPlayArea", object) - end - - PlayAreaApi.getSurface = function() - return getPlayArea().getCustomObject().image - end - - PlayAreaApi.updateSurface = function(url) - return getPlayArea().call("updateSurface", url) - end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the - -- data to the local token manager instance. - ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call - PlayAreaApi.updateLocations = function(args) - getPlayArea().call("updateLocations", args) - end - - PlayAreaApi.getCustomDataHelper = function() - return getPlayArea().getVar("customDataHelper") - end - - return PlayAreaApi + return SearchLib end end) __bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -894,6 +924,7 @@ do local guidReferenceApi = require("core/GUIDReferenceApi") local optionPanelApi = require("core/OptionPanelApi") local playAreaApi = require("core/PlayAreaApi") + local playermatApi = require("playermat/PlayermatApi") local searchLib = require("util/SearchLib") local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi") @@ -1024,13 +1055,13 @@ do local TokenManager = {} local internal = {} - -- Spawns tokens for the card. This function is built to just throw a card at it and let it do - -- the work once a card has hit an area where it might spawn tokens. It will check to see if + -- Spawns tokens for the card. This function is built to just throw a card at it and let it do + -- the work once a card has hit an area where it might spawn tokens. It will check to see if -- the card has already spawned, find appropriate data from either the uses metadata or the Data -- Helper, and spawn the tokens. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 TokenManager.spawnForCard = function(card, extraUses) if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then return @@ -1045,11 +1076,11 @@ do -- Spawns a set of tokens on the given card. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string Type of token to spawn, for example "damage", "horror" or "resource" - ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the -- spawned state object rather than spawning multiple tokens ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType) local optionPanel = optionPanelApi.getOptions() @@ -1064,18 +1095,21 @@ do end end - -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror - -- tokens. + -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens. ---@param card tts__Object Card to spawn tokens on - ---@param tokenType string type of token to spawn, valid values are "damage" and "horror". Other - -- types should use spawnMultipleTokens() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenValue number Value to set the damage/horror to TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown) if tokenValue < 1 or tokenValue > 50 then return end local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown)) local rot = card.getRotation() - TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end) + TokenManager.spawnToken(pos, tokenType, rot, function(spawned) + -- token starts in state 1, so don't attempt to change it to avoid error + if tokenValue ~= 1 then + spawned.setState(tokenValue) + end + end) end TokenManager.spawnResourceCounterToken = function(card, tokenCount) @@ -1087,11 +1121,10 @@ do end -- Spawns a number of tokens. - ---@param tokenType string type of token to spawn, valid values are resource", "doom", or "clue". - -- Other types should use spawnCounterToken() + ---@param tokenType string Type of token to spawn (template needs to be in source bag) ---@param tokenCount number How many tokens to spawn ---@param shiftDown? number An offset for the z-value of this group of tokens - ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens + ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType) -- not checking the max at this point since clue offsets are calculated dynamically if tokenCount < 1 then return end @@ -1101,7 +1134,11 @@ do offsets = internal.buildClueOffsets(card, tokenCount) else -- only up to 12 offset tables defined - if tokenCount > 12 then return end + if tokenCount > 12 then + printToAll("Attempting to spawn " .. tokenCount .. " tokens. Spawning clickable counter instead.") + TokenManager.spawnResourceCounterToken(card, tokenCount) + return + end for i = 1, tokenCount do offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i]) -- Fix the y-position for the spawn, since positionToWorld considers rotation which can @@ -1127,16 +1164,16 @@ do return end - -- handling for not provided subtype (for example when spawning from custom data helpers) - if subType == nil then - subType = "" - end - -- this is used to load the correct state for additional resource tokens (e.g. "Ammo") local callback = nil - local stateID = stateTable[string.lower(subType)] + local stateID = stateTable[string.lower(subType or "")] if tokenType == "resource" and stateID ~= nil and stateID ~= 1 then callback = function(spawned) spawned.setState(stateID) end + elseif tokenType == "universalActionAbility" then + local matColor = playermatApi.getMatColorByPosition(card.getPosition()) + local class = playermatApi.returnInvestigatorClass(matColor) + + callback = function(spawned) spawned.call("updateClassAndSymbol", { class = class, symbol = subType or class }) end end for i = 1, tokenCount do @@ -1146,9 +1183,8 @@ do -- Spawns a single token at the given global position by copying it from the template bag. ---@param position tts__Vector Global position to spawn the token - ---@param tokenType string type of token to spawn, valid values are "damage", "horror", - -- "resource", "doom", or "clue" - ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, + ---@param tokenType string Type of token to spawn (template needs to be in source bag) + ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used, -- x and z will use the default rotation from the source bag ---@param callback? function A callback function triggered after the new token is spawned TokenManager.spawnToken = function(position, tokenType, rotation, callback) @@ -1184,21 +1220,13 @@ do -- Checks a card for metadata to maybe replenish it ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - TokenManager.maybeReplenishCard = function(card, uses, mat) + TokenManager.maybeReplenishCard = function(card, uses) -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that) if uses[1].count and uses[1].replenish then - internal.replenishTokens(card, uses, mat) + internal.replenishTokens(card, uses) end end - -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some - -- callers. - ---@param card tts__Object Card object to reset the tokens for - TokenManager.resetTokensSpawned = function(card) - tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID()) - end - -- Pushes new player card data into the local copy of the Data Helper player data. ---@param dataTable table Key/Value pairs following the DataHelper style TokenManager.addPlayerCardData = function(dataTable) @@ -1237,7 +1265,7 @@ do end end - -- Copies the data from the DataHelper. Will only happen once. + -- Copies the data from the DataHelper. Will only happen once. internal.initDataHelperData = function() if playerCardData ~= nil then return @@ -1247,11 +1275,11 @@ do locationData = dataHelper.getTable('LOCATIONS_DATA') end - -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state + -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for ---@param extraUses table A table of = which will modify the number of tokens - --- spawned for that type. e.g. Akachi's playmat should pass "Charge"=1 + --- spawned for that type. e.g. Akachi's playermat should pass "Charge"=1 internal.spawnTokensFromUses = function(card, extraUses) local uses = internal.getUses(card) if uses == nil then return end @@ -1270,7 +1298,7 @@ do tokenSpawnTrackerApi.markTokensSpawned(card.getGUID()) end - -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state + -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state -- of the card for both locations and standard cards. ---@param card tts__Object Card to maybe spawn tokens for internal.spawnTokensFromDataHelper = function(card) @@ -1287,7 +1315,7 @@ do -- Spawn tokens for a player card using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param playerData table Player card data structure retrieved from the DataHelper. Should be + ---@param playerData table Player card data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData) local token = playerData.tokenType @@ -1298,7 +1326,7 @@ do -- Spawn tokens for a location using data retrieved from the Data Helper. ---@param card tts__Object Card to maybe spawn tokens for - ---@param locationData table Location data structure retrieved from the DataHelper. Should be + ---@param locationData table Location data structure retrieved from the DataHelper. Should be -- the right data for this card. internal.spawnLocationTokensFromDataHelper = function(card, locationData) local clueCount = internal.getClueCountFromData(card, locationData) @@ -1371,21 +1399,16 @@ do ---@param card tts__Object Card object to be replenished ---@param uses table The already decoded metadata.uses (to avoid decoding again) - ---@param mat tts__Object The playmat the card is placed on (for rotation and casting) - internal.replenishTokens = function(card, uses, mat) - local cardPos = card.getPosition() - - -- don't continue for cards on the deck (Norman) or in the discard pile - if mat.positionToLocal(cardPos).x < -1 then return end - - -- get current amount of resource tokens on the card + internal.replenishTokens = function(card, uses) + -- get current amount of matching resource tokens on the card local clickableResourceCounter = nil local foundTokens = 0 + local searchType = string.lower(uses[1].type) for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do local memo = obj.getMemo() - if (stateTable[memo] or 0) > 0 then + if searchType == memo then foundTokens = foundTokens + math.abs(obj.getQuantity()) obj.destruct() elseif memo == "resourceCounter" then @@ -1417,6 +1440,24 @@ do return TokenManager end end) +__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local OptionPanelApi = {} + + -- loads saved options + ---@param options table Set a new state for the option table + OptionPanelApi.loadSettings = function(options) + return Global.call("loadSettings", options) + end + + ---@return any: Table of option panel state + OptionPanelApi.getOptions = function() + return Global.getTable("optionPanel") + end + + return OptionPanelApi +end +end) __bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local TokenSpawnTracker = {} @@ -1434,8 +1475,8 @@ do return getSpawnTracker().call("markTokensSpawned", cardGuid) end - TokenSpawnTracker.resetTokensSpawned = function(cardGuid) - return getSpawnTracker().call("resetTokensSpawned", cardGuid) + TokenSpawnTracker.resetTokensSpawned = function(card) + return getSpawnTracker().call("resetTokensSpawned", card) end TokenSpawnTracker.resetAllAssetAndEvents = function() @@ -1453,75 +1494,417 @@ do return TokenSpawnTracker end end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } + local PlayermatApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] + -- Convenience function to look up a mat's object by color, or get all mats. + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested + local function getMatForColor(matColor) + if matColor == "All" then + return guidReferenceApi.getObjectsByType("Playermat") + else + return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") } end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) + end - -- filtering the result + -- Returns the color of the closest playermat + ---@param startPos table Starting position to get the closest mat from + PlayermatApi.getMatColorByPosition = function(startPos) + local result, smallestDistance + for matColor, mat in pairs(getMatForColor("All")) do + local distance = Vector.between(startPos, mat.getPosition()):magnitude() + if smallestDistance == nil or distance < smallestDistance then + smallestDistance = distance + result = matColor + end + end + return result + end + + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("playerColor") + end + end + + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) + for matColor, mat in pairs(getMatForColor("All")) do + local playerColor = mat.getVar("playerColor") + if playerColor == handColor then + return matColor + end + end + end + + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("checkForDES") + end + end + + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getDeckAreaObjects") + end + end + + -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("flipTopCardFromDeck") + end + end + + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDiscardPosition") + end + end + + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + + -- Transforms a local position into a global position + ---@param localPos table Local position to be transformed + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.positionToWorld(localPos) + end + end + + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getRotation() + end + end + + -- Returns a table with spawn data (position and rotation) for a helper object + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param helperName string Name of the helper object + PlayermatApi.getHelperSpawnData = function(matColor, helperName) + local resultTable = {} + local localPositionTable = { + ["Hand Helper"] = {0.05, 0, -1.182}, + ["Search Assistant"] = {-0.3, 0, -1.182} + } + + for color, mat in pairs(getMatForColor(matColor)) do + resultTable[color] = { + position = mat.positionToWorld(localPositionTable[helperName]), + rotation = mat.getRotation() + } + end + return resultTable + end + + + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param playerColor string Color of the calling player (for messages) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doUpkeepFromHotkey", playerColor) + end + end + + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param objList table List of objects to discard + PlayermatApi.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 playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorId") + end + end + + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If + -- matchTypes is true, the main card slot snap points will only snap assets, while the + -- investigator area point will only snap Investigators. If matchTypes is false, snap points will + -- be reset to snap all cards. + ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("setLimitSnapsByType", matchCardTypes) + end + end + + -- Sets the requested playermat's draw 1 button to visible + ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("showDrawButton", isDrawButtonVisible) + end + end + + -- Shows or hides the clickable clue counter for the requested playermat + ---@param showCounter boolean Whether the clickable counter should be present or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("clickableClues", showCounter) + end + end + + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("removeClues") + end + end + + -- Reports the clue count for the requested playermat + ---@param useClickableCounters boolean Controls which type of counter is getting checked + PlayermatApi.getClueCount = function(useClickableCounters, matColor) + local count = 0 + for _, mat in pairs(getMatForColor(matColor)) do + count = count + mat.call("getClueCount", useClickableCounters) + end + return count + end + + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param type string Counter to target + ---@param newValue number Value to set the counter to + ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) + end + end + + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param number number Amount of cards to draw + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("drawCardsWithReshuffle", number) + end + end + + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param type string Counter to target + PlayermatApi.getCounterValue = function(matColor, type) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("getCounterValue", type) + end + end + + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() + local usedColors = {} + for matColor, mat in pairs(getMatForColor("All")) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult > 0 then + table.insert(usedColors, matColor) + end + end + return usedColors + end + + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("resetSkillTracker") + end + end + + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@param filter string Name of the filte function (see util/SearchLib) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) + for _, mat in pairs(getMatForColor(matColor)) do + for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do + table.insert(objList, obj) end end return objList end - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) + -- Discard a non-hidden card from the corresponding player's hand + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("doDiscardOne") + end end - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() + for _, mat in pairs(getMatForColor("All")) do + mat.call("syncAllCustomizableCards") + end end - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) + return PlayermatApi +end +end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/PlayArea") +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") end - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - return SearchLib + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi end end) return __bundle_require("__root") diff --git a/unpacked/Custom_Token Playmat Image Swapper b7b45b.ttslua b/unpacked/Custom_Token PlayArea Image Swapper b7b45b.ttslua similarity index 97% rename from unpacked/Custom_Token Playmat Image Swapper b7b45b.ttslua rename to unpacked/Custom_Token PlayArea Image Swapper b7b45b.ttslua index bd9dcf859..7f918ba8a 100644 --- a/unpacked/Custom_Token Playmat Image Swapper b7b45b.ttslua +++ b/unpacked/Custom_Token PlayArea Image Swapper b7b45b.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/PlayAreaSelector") -end) __bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local GUIDReferenceApi = {} @@ -52,6 +49,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -59,21 +57,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -85,9 +83,235 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/PlayAreaSelector") +end) +__bundle_register("core/PlayAreaSelector", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/PlayAreaImageData") -- this fills the variable "PLAYAREA_IMAGE_DATA" +local optionPanelApi = require("core/OptionPanelApi") +local playAreaApi = require("core/PlayAreaApi") +local typeIndex, selectionIndex, plainNameCache + +function onSave() + return JSON.encode({ + typeIndex = typeIndex, + selectionIndex = selectionIndex + }) +end + +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) or {} + typeIndex = loadedData.typeIndex or 1 + selectionIndex = loadedData.selectionIndex or 1 + end + + self.createButton({ + function_owner = self, + click_function = "onClick_toggleGallery", + tooltip = "Show Image Gallery", + position = {0, 0.06, 0}, + height = 1500, + width = 1500, + color = { 1, 1, 1, 0 } + }) + + Wait.time(updatePlayAreaGallery, 0.5) + math.randomseed(os.time()) +end + +-- click function for main button +function onClick_toggleGallery(_, playerColor) + Global.call("togglePlayAreaGallery", playerColor) +end + +function getDataSubTableByIndex(dataTable, index) + local loopId = 1 + for i, v in pairs(dataTable) do + if index == loopId then return v end + loopId = loopId + 1 + end + return {} +end + +function updatePlayAreaGallery() + -- get subtables + local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex) + local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex) + + -- get global xml to insert elements + local globalXml = UI.getXmlTable() + + -- selectable items + local itemSelection = getXmlTableElementById(globalXml, 'itemSelection') + itemSelection.children = {} + + local i = 0 + for itemName, _ in pairs(dataForType) do + i = i + 1 + table.insert(itemSelection.children, + { + tag = "Panel", + attributes = { class = "itemPanel", id = "typePanel" .. i }, + children = { + tag = "Text", + value = itemName, + attributes = { class = "itemText", id = "typeListText" .. i } + } + }) + end + + -- selectable images for that item + local playareaList = getXmlTableElementById(globalXml, 'playareaList') + playareaList.children = {} + + for i, v in ipairs(dataForSelection) do + table.insert(playareaList.children, + { + tag = "VerticalLayout", + attributes = { class = "imageBox", id = "image" .. i }, + children = { + { + tag = 'Image', + attributes = { class = "playareaImage", image = v.URL } + }, + { + tag = 'Text', + value = v.Name, + attributes = { class = "imageName" } + } + } + }) + end + + playareaList.attributes.height = round(#playareaList.children / 2, 0) * 380 + Global.call("updateGlobalXml", globalXml) + Wait.time(highlightTabAndItem, 0.1) +end + +function onClick_imageTab(_, _, tabId) + typeIndex = tonumber(tabId:sub(9)) + selectionIndex = 1 + updatePlayAreaGallery() +end + +function onClick_listItem(_, _, listId) + selectionIndex = tonumber(listId:sub(10)) + updatePlayAreaGallery() +end + +function onClick_image(player, _, id) + local imageIndex = tonumber(id:sub(6)) + local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex) + local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex) + local newURL = dataForSelection[imageIndex].URL + playAreaApi.updateSurface(newURL) + Global.call("togglePlayAreaGallery", player.color) +end + +function highlightTabAndItem() + -- highlight active tab + for i = 1, 5 do + local color = "#888888" + if i == typeIndex then color = "#ffffff" end + UI.setAttribute("imageTab" .. i, "color", color) + end + + -- highlight item + UI.setAttribute("typePanel" .. selectionIndex, "color", "grey") + UI.setAttribute("typeListText" .. selectionIndex, "color", "black") +end + +-- loops through an XML table and returns the specified object +---@param ui table XmlTable (get this via getXmlTable) +---@param id string Id of the object to return +function getXmlTableElementById(ui, id) + for _, obj in ipairs(ui) do + if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end + if obj.children then + local result = getXmlTableElementById(obj.children, id) + if result then return result end + end + end + return nil +end + +-- utility function +function round(num, numDecimalPlaces) + local mult = 10 ^ (numDecimalPlaces or 0) + return math.floor(num * mult + 0.5) / mult +end + +function maybeUpdatePlayAreaImage(scenarioName) + -- check if option is enabled + local optionPanelState = optionPanelApi.getOptions() + if not optionPanelState["changePlayAreaImage"] then return end + + -- initialize cache if nil + if not plainNameCache then + plainNameCache = {} + for i, dataForType in pairs(PLAYAREA_IMAGE_DATA) do + for j, dataForCycle in pairs(dataForType) do + for k, data in ipairs(dataForCycle) do + local plainName = getPlainName(data.Name) + + -- override plainName for all images in the "Other Images" category (except the default image) + if i == "Other Images" and data.Name ~= "Default Image" then + plainName = "Generic" + end + + if not plainNameCache[plainName] then + plainNameCache[plainName] = {} + end + table.insert(plainNameCache[plainName], data.URL) + end + end + end + end + + -- look for matching playarea image or use generic ones instead + local listOfEligibleImages = {} + if plainNameCache[scenarioName] then + listOfEligibleImages = plainNameCache[scenarioName] + else + listOfEligibleImages = plainNameCache["Generic"] + end + + -- get a random entry from the eligible list + local newImageIndex = math.random(#listOfEligibleImages) + playAreaApi.updateSurface(listOfEligibleImages[newImageIndex]) +end + +-- attempts to extract the plain scenario name from the playarea image name +function getPlainName(str) + -- remove prefix type 1 + str = str:gsub("%w+%-%w%s%-%s", "") -- matches "II-B - Thousand Shapes of Horror 1" + + -- remove prefix type 2 + str = str:gsub("%w+%-%w%s", "") -- matches "59-Z Congress of Keys 1" + + -- remove prefix type 3 + str = str:gsub("%w+%s%-%s", "") -- matches "III - The Secret Name 4" + + -- remove prefix type 4 + str = str:gsub("%?+%s%-%s", "") -- matches "??? - Fatal Mirage" + + -- remove suffix (numbering) + str = str:gsub("%s%d+", "") + + return str +end +end) __bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local OptionPanelApi = {} @@ -119,13 +343,13 @@ do return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end - -- Returns the current value of the investigator counter from the playmat + -- Returns the current value of the investigator counter from the playermat ---@return number: Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getInvestigatorCounter().getVar("val") end - -- Updates the current value of the investigator counter from the playmat + -- Updates the current value of the investigator counter from the playermat ---@param count number Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) getInvestigatorCounter().call("updateVal", count) @@ -166,7 +390,7 @@ do getPlayArea().call("onScenarioChanged", scenarioName) end - -- Sets this playmat's snap points to limit snapping to locations or not. + -- Sets this playermat's snap points to limit snapping to locations or not. -- If matchTypes is false, snap points will be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) @@ -179,18 +403,18 @@ do getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) end - -- counts the VP on locations in the play area + -- Counts the VP on locations in the play area PlayAreaApi.countVP = function() return getPlayArea().call("countVP") end - -- highlights all locations in the play area without metadata + -- Highlights all locations in the play area without metadata ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightMissingData = function(state) return getPlayArea().call("highlightMissingData", state) end - - -- highlights all locations in the play area with VP + + -- Highlights all locations in the play area with VP ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightCountedVP = function(state) return getPlayArea().call("countVP", state) @@ -201,15 +425,26 @@ do return getPlayArea().call("isInPlayArea", object) end + -- Returns the current surface of the play area PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end + -- Updates the surface of the play area PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call PlayAreaApi.updateLocations = function(args) @@ -580,7 +815,7 @@ PLAYAREA_IMAGE_DATA = { URL = "http://cloud-3.steamusercontent.com/ugc/2279446315725460182/C086EDA78636624FC9C4EA1DBCD8F1D39B7A6A89/" }, { - Name = "VI - Union and Disillusioned", + Name = "VI - Union and Disillusion", URL = "http://cloud-3.steamusercontent.com/ugc/2279446315725460382/8EFB842A623A2D1E6D0E915268A207A5D7DFC6D4/" }, { @@ -1235,212 +1470,4 @@ PLAYAREA_IMAGE_DATA = { } } end) -__bundle_register("core/PlayAreaSelector", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/PlayAreaImageData") -- this fills the variable "PLAYAREA_IMAGE_DATA" -local optionPanelApi = require("core/OptionPanelApi") -local playAreaApi = require("core/PlayAreaApi") -local typeIndex, selectionIndex, plainNameCache - -function onSave() return JSON.encode({ typeIndex = typeIndex, selectionIndex = selectionIndex }) end - -function onLoad(savedData) - self.createButton({ - function_owner = self, - click_function = "onClick_toggleGallery", - tooltip = "Show Image Gallery", - position = {0, 0.06, 0}, - height = 1500, - width = 1500, - color = { 1, 1, 1, 0 } - }) - - local loadedData = JSON.decode(savedData) or {} - typeIndex = loadedData.typeIndex or 1 - selectionIndex = loadedData.selectionIndex or 1 - Wait.time(updatePlayAreaGallery, 0.5) - math.randomseed(os.time()) -end - --- click function for main button -function onClick_toggleGallery(_, playerColor) - Global.call("togglePlayAreaGallery", playerColor) -end - -function getDataSubTableByIndex(dataTable, index) - local loopId = 1 - for i, v in pairs(dataTable) do - if index == loopId then return v end - loopId = loopId + 1 - end - return {} -end - -function updatePlayAreaGallery() - -- get subtables - local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex) - local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex) - - -- get global xml to insert elements - local globalXml = UI.getXmlTable() - - -- selectable items - local itemSelection = getXmlTableElementById(globalXml, 'itemSelection') - itemSelection.children = {} - - local i = 0 - for itemName, _ in pairs(dataForType) do - i = i + 1 - table.insert(itemSelection.children, - { - tag = "Panel", - attributes = { class = "itemPanel", id = "typePanel" .. i }, - children = { - tag = "Text", - value = itemName, - attributes = { class = "itemText", id = "typeListText" .. i } - } - }) - end - - -- selectable images for that item - local playareaList = getXmlTableElementById(globalXml, 'playareaList') - playareaList.children = {} - - for i, v in ipairs(dataForSelection) do - table.insert(playareaList.children, - { - tag = "VerticalLayout", - attributes = { class = "imageBox", id = "image" .. i }, - children = { - { - tag = 'Image', - attributes = { class = "playareaImage", image = v.URL } - }, - { - tag = 'Text', - value = v.Name, - attributes = { class = "imageName" } - } - } - }) - end - - playareaList.attributes.height = round(#playareaList.children / 2, 0) * 380 - Global.call("updateGlobalXml", globalXml) - Wait.time(highlightTabAndItem, 0.1) -end - -function onClick_imageTab(_, _, tabId) - typeIndex = tonumber(tabId:sub(9)) - selectionIndex = 1 - updatePlayAreaGallery() -end - -function onClick_listItem(_, _, listId) - selectionIndex = tonumber(listId:sub(10)) - updatePlayAreaGallery() -end - -function onClick_image(player, _, id) - local imageIndex = tonumber(id:sub(6)) - local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex) - local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex) - local newURL = dataForSelection[imageIndex].URL - playAreaApi.updateSurface(newURL) - Global.call("togglePlayAreaGallery", player.color) -end - -function highlightTabAndItem() - -- highlight active tab - for i = 1, 5 do - local color = "#888888" - if i == typeIndex then color = "#ffffff" end - UI.setAttribute("imageTab" .. i, "color", color) - end - - -- highlight item - UI.setAttribute("typePanel" .. selectionIndex, "color", "grey") - UI.setAttribute("typeListText" .. selectionIndex, "color", "black") -end - --- loops through an XML table and returns the specified object ----@param ui table XmlTable (get this via getXmlTable) ----@param id string Id of the object to return -function getXmlTableElementById(ui, id) - for _, obj in ipairs(ui) do - if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end - if obj.children then - local result = getXmlTableElementById(obj.children, id) - if result then return result end - end - end - return nil -end - --- utility function -function round(num, numDecimalPlaces) - local mult = 10 ^ (numDecimalPlaces or 0) - return math.floor(num * mult + 0.5) / mult -end - -function maybeUpdatePlayAreaImage(scenarioName) - -- check if option is enabled - local optionPanelState = optionPanelApi.getOptions() - if not optionPanelState["changePlayAreaImage"] then return end - - -- initialize cache if nil - if not plainNameCache then - plainNameCache = {} - for i, dataForType in pairs(PLAYAREA_IMAGE_DATA) do - for j, dataForCycle in pairs(dataForType) do - for k, data in ipairs(dataForCycle) do - local plainName = getPlainName(data.Name) - - -- override plainName for all images in the "Other Images" category (except the default image) - if i == "Other Images" and data.Name ~= "Default Image" then - plainName = "Generic" - end - - if not plainNameCache[plainName] then - plainNameCache[plainName] = {} - end - table.insert(plainNameCache[plainName], data.URL) - end - end - end - end - - -- look for matching playarea image or use generic ones instead - local listOfEligibleImages = {} - if plainNameCache[scenarioName] then - listOfEligibleImages = plainNameCache[scenarioName] - else - listOfEligibleImages = plainNameCache["Generic"] - end - - -- get a random entry from the eligible list - local newImageIndex = math.random(#listOfEligibleImages) - playAreaApi.updateSurface(listOfEligibleImages[newImageIndex]) -end - --- attempts to extract the plain scenario name from the playarea image name -function getPlainName(str) - -- remove prefix type 1 - str = str:gsub("%w+%-%w%s%-%s", "") -- matches "II-B - Thousand Shapes of Horror 1" - - -- remove prefix type 2 - str = str:gsub("%w+%-%w%s", "") -- matches "59-Z Congress of Keys 1" - - -- remove prefix type 3 - str = str:gsub("%w+%s%-%s", "") -- matches "III - The Secret Name 4" - - -- remove prefix type 4 - str = str:gsub("%?+%s%-%s", "") -- matches "??? - Fatal Mirage" - - -- remove suffix (numbering) - str = str:gsub("%s%d+", "") - - return str -end -end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Playmat Image Swapper b7b45b.yaml b/unpacked/Custom_Token PlayArea Image Swapper b7b45b.yaml similarity index 81% rename from unpacked/Custom_Token Playmat Image Swapper b7b45b.yaml rename to unpacked/Custom_Token PlayArea Image Swapper b7b45b.yaml index 8e87a947e..a468d1c43 100644 --- a/unpacked/Custom_Token Playmat Image Swapper b7b45b.yaml +++ b/unpacked/Custom_Token PlayArea Image Swapper b7b45b.yaml @@ -17,7 +17,7 @@ CustomImage: ImageSecondaryURL: '' ImageURL: https://i.imgur.com/gs1mtXJ.png WidthScale: 0 -Description: Allows changing of the playmat image. Provide URL to the image or leave +Description: Allows changing of the playarea image. Provide URL to the image or leave empty for default image. DragSelectable: true GMNotes: '' @@ -29,11 +29,11 @@ HideWhenFaceDown: false IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true -LuaScript: !include 'Custom_Token Playmat Image Swapper b7b45b.ttslua' +LuaScript: !include 'Custom_Token PlayArea Image Swapper b7b45b.ttslua' LuaScriptState: '{"selectionIndex":1,"typeIndex":1}' MeasureMovement: false Name: Custom_Token -Nickname: Playmat Image Swapper +Nickname: PlayArea Image Swapper Snap: true Sticky: true Tags: diff --git a/unpacked/Custom_Token Resources 4406f0.ttslua b/unpacked/Custom_Token Resources 4406f0.ttslua index 3e8cdcc2f..7f418bf5b 100644 --- a/unpacked/Custom_Token Resources 4406f0.ttslua +++ b/unpacked/Custom_Token Resources 4406f0.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/GenericCounter") -end) __bundle_register("core/GenericCounter", function(require, _LOADED, __bundle_register, __bundle_modules) MIN_VALUE = 0 MAX_VALUE = 99 @@ -52,21 +49,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +78,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) @@ -100,4 +97,7 @@ function addOrSubtract(_, _, isRightClick) self.editButton({ index = 0, label = tostring(val) }) end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/GenericCounter") +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Resources 816d84.ttslua b/unpacked/Custom_Token Resources 816d84.ttslua index 3e8cdcc2f..7f418bf5b 100644 --- a/unpacked/Custom_Token Resources 816d84.ttslua +++ b/unpacked/Custom_Token Resources 816d84.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/GenericCounter") -end) __bundle_register("core/GenericCounter", function(require, _LOADED, __bundle_register, __bundle_modules) MIN_VALUE = 0 MAX_VALUE = 99 @@ -52,21 +49,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +78,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) @@ -100,4 +97,7 @@ function addOrSubtract(_, _, isRightClick) self.editButton({ index = 0, label = tostring(val) }) end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/GenericCounter") +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Resources a4b60d.ttslua b/unpacked/Custom_Token Resources a4b60d.ttslua index 3e8cdcc2f..f073bb14e 100644 --- a/unpacked/Custom_Token Resources a4b60d.ttslua +++ b/unpacked/Custom_Token Resources a4b60d.ttslua @@ -52,21 +52,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +81,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) diff --git a/unpacked/Custom_Token Resources cd15ac.ttslua b/unpacked/Custom_Token Resources cd15ac.ttslua index 3e8cdcc2f..7f418bf5b 100644 --- a/unpacked/Custom_Token Resources cd15ac.ttslua +++ b/unpacked/Custom_Token Resources cd15ac.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/GenericCounter") -end) __bundle_register("core/GenericCounter", function(require, _LOADED, __bundle_register, __bundle_modules) MIN_VALUE = 0 MAX_VALUE = 99 @@ -52,21 +49,20 @@ val = 0 function onSave() return JSON.encode(val) end function onLoad(savedData) - if savedData ~= nil then + if savedData and savedData ~= "" then val = JSON.decode(savedData) end local name = self.getName() - local position = {} + local position = { 0, 0.06, 0 } + -- set position of label depending on object if name == "Damage" or name == "Resources" or name == "Resource Counter" then position = { 0, 0.06, 0.1 } elseif name == "Horror" then position = { -0.025, 0.06, -0.025 } elseif name == "Elder Sign Counter" or name == "Auto-fail Counter" then position = { 0, 0.1, 0 } - else - position = { 0, 0.06, 0 } end self.createButton({ @@ -82,6 +78,7 @@ function onLoad(savedData) color = { 0, 0, 0, 0 } }) + -- add context menu entries self.addContextMenuItem("Add 5", function() updateVal(val + 5) end) self.addContextMenuItem("Subtract 5", function() updateVal(val - 5) end) self.addContextMenuItem("Add 10", function() updateVal(val + 10) end) @@ -100,4 +97,7 @@ function addOrSubtract(_, _, isRightClick) self.editButton({ index = 0, label = tostring(val) }) end end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/GenericCounter") +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Token Arranger 022907.ttslua b/unpacked/Custom_Token Token Arranger 022907.ttslua index bf8c08b45..4352f2607 100644 --- a/unpacked/Custom_Token Token Arranger 022907.ttslua +++ b/unpacked/Custom_Token Token Arranger 022907.ttslua @@ -59,16 +59,18 @@ buttonParameters.height = 325 local inputParameters = {} inputParameters.function_owner = self -inputParameters.font_size = 100 -inputParameters.width = 250 -inputParameters.height = inputParameters.font_size + 23 +inputParameters.font_size = 200 +inputParameters.width = 500 +inputParameters.height = inputParameters.font_size + 46 inputParameters.alignment = 3 inputParameters.validation = 2 inputParameters.tab = 2 +inputParameters.scale = { 0.5, 0.5, 0.5 } local percentageLabel = {} percentageLabel.function_owner = self percentageLabel.click_function = "none" +percentageLabel.font_size = 200 percentageLabel.width = 0 percentageLabel.height = 0 @@ -102,9 +104,9 @@ function onSave() end -- loading data, button creation and initial layouting -function onLoad(saveState) - if saveState ~= nil and saveState ~= "" then - local loadedData = JSON.decode(saveState) +function onLoad(savedData) + if savedData and savedData ~= "" then + local loadedData = JSON.decode(savedData) tokenPrecedence = loadedData.tokenPrecedence percentage = loadedData.percentage includeDrawnTokens = loadedData.includeDrawnTokens @@ -116,13 +118,13 @@ function onLoad(saveState) end createButtonsAndInputs() - + -- maybe trigger layout() to draw percentage buttons local objList = getObjectsWithTag("tempToken") if #objList > 0 then Wait.time(layout, 0.5) end - + -- context menu items self.addContextMenuItem("Load default values", function() loadDefaultValues() @@ -200,10 +202,10 @@ function loadDefaultValues() ["Tablet"] = { -3, 5}, ["Elder Thing"] = { -4, 6}, ["Auto-fail"] = { -100, 7}, - ["Bless"] = { 101, 8}, - ["Curse"] = { -101, 9}, - ["Frost"] = { -99, 10}, - [""] = { 0, 11} + ["Bless"] = { 110, 8}, + ["Curse"] = { -110, 9}, + ["Frost"] = { -105, 10}, + [""] = { 0, 11} } end @@ -242,9 +244,11 @@ function createButtonsAndInputs() click_function = "layout", tooltip = "Left-Click: Update!\nRight-Click: Hide Tokens!", position = { 0.725, 0.1, 2.025 }, + scale = { 0.5, 0.5, 0.5 }, color = { 1, 1, 1 }, - width = 675, - height = 175 + font_size = 200, + width = 1350, + height = 325 }) end @@ -285,14 +289,14 @@ function deleteCopiedTokens() end -- creates buttons as labels as display for percentage values -function createPercentageButton(tokenCount, valueCount, tokenName) - local startPos = Vector(2.3, -0.04, 0.875 * valueCount) +function createPercentageButton(tokenCount, rowCount, tokenName) + local startPos = Vector(2.3, -0.04, 0.875 * rowCount) if percentage == "cumulative" then - percentageLabel.scale = { 1.5, 1.5, 1.5 } + percentageLabel.scale = { 0.75, 0.75, 0.75 } percentageLabel.position = startPos - Vector(0, 0, 2.85) else - percentageLabel.scale = { 2, 2, 2 } + percentageLabel.scale = { 1, 1, 1 } percentageLabel.position = startPos - Vector(0, 0, 2.675) end @@ -301,8 +305,8 @@ function createPercentageButton(tokenCount, valueCount, tokenName) percentageLabel.font_color = { 0.35, 0.71, 0.85 } elseif tokenName == "Auto-fail" then percentageLabel.font_color = { 0.86, 0.1, 0.1 } - -- check if the tokenName contains letters (e.g. symbol token) elseif string.match(tokenName, "%a") ~= nil then + -- tokenName contains letters (e.g. symbol token) percentageLabel.font_color = { 0.68, 0.53, 0.86 } else percentageLabel.font_color = { 0.85, 0.67, 0.33 } @@ -349,12 +353,14 @@ function layout(_, _, isRightClick) end -- clone tokens from chaos bag (default position above trash can) - local rawData = chaosBag.getData().ContainedObjects + local rawData = chaosBag.getData().ContainedObjects or {} -- optionally get the data for tokens in play if includeDrawnTokens then for _, token in pairs(chaosBagApi.getTokensInPlay()) do - if token ~= nil then table.insert(rawData, token.getData()) end + if token ~= nil then + table.insert(rawData, token.getData()) + end end end @@ -382,8 +388,15 @@ function layout(_, _, isRightClick) end end - -- sort table by value (symbols last if same value) - table.sort(data, tokenValueComparator) + -- handling for near empty chaos bag + if #data == 0 then + -- small delay to limit update calls + Wait.time(function() updating = false end, 0.1) + return + elseif #data > 1 then + -- sort table by value (symbols last if same value) + table.sort(data, tokenValueComparator) + end -- laying out the tokens local pos = self.getPosition() + Vector(3.55, -0.05, -3.95) @@ -393,21 +406,21 @@ function layout(_, _, isRightClick) local rotation = self.getRotation() local currentValue = data[1].value local tokenCount = { row = 0, sum = 0, total = #data } - local valueCount = 1 - local tokenName = false + local rowCount = 1 + local tokenName - for i, item in ipairs(data) do + for _, item in ipairs(data) do -- this is true for the first token in a new row if item.value ~= currentValue then if percentage then tokenCount.sum = tokenCount.sum + tokenCount.row - createPercentageButton(tokenCount, valueCount, tokenName) + createPercentageButton(tokenCount, rowCount, tokenName) end location.x = location.x - 1.75 location.z = pos.z currentValue = item.value - valueCount = valueCount + 1 + rowCount = rowCount + 1 tokenCount.row = 0 end @@ -424,21 +437,17 @@ function layout(_, _, isRightClick) -- this is repeated to create the button for the last token if percentage then tokenCount.sum = tokenCount.sum + tokenCount.row - createPercentageButton(tokenCount, valueCount, tokenName) + createPercentageButton(tokenCount, rowCount, tokenName) end - -- introducing a small delay to limit update calls + -- small delay to limit update calls Wait.time(function() updating = false end, 0.1) end -- called from outside to set default values for tokens function onTokenDataChanged(parameters) - local tokenData = parameters.tokenData or {} - local currentScenario = parameters.currentScenario or "" - local useFrontData = parameters.useFrontData - -- update token precedence - for key, table in pairs(tokenData) do + for key, table in pairs(parameters.tokenData or {}) do local modifier = table.modifier if modifier == -999 then modifier = 0 end tokenPrecedence[key][1] = modifier @@ -455,7 +464,7 @@ do -- respawns the chaos bag with a new state of tokens ---@param tokenList table List of chaos token ids ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) + Global.call("setChaosBagState", tokenList) end -- returns a Table List of chaos token ids in the current chaos bag @@ -482,48 +491,57 @@ do -- returns all sealed tokens on cards to the chaos bag ---@param playerColor string Color of the player to show the broadcast to ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) + Global.call("releaseAllSealedTokens", playerColor) end -- returns all drawn tokens to the chaos bag ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") + Global.call("returnChaosTokens") end -- removes the specified chaos token from the chaos bag ---@param id string ID of the chaos token ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) + Global.call("removeChaosToken", id) end -- returns a chaos token to the bag and calls all relevant functions ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) end -- spawns the specified chaos token and puts it into the chaos bag ---@param id string ID of the chaos token ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) + Global.call("spawnChaosToken", id) end -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the -- contents of the bag should check this method before doing so. -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. + ---@return any: True if the bag is manipulated, false if it should be blocked. ChaosBagApi.canTouchChaosTokens = function() return Global.call("canTouchChaosTokens") end - -- called by playermats (by the "Draw chaos token" button) + -- draws a chaos token to a playermat ---@param mat tts__Object Playermat that triggered this ---@param drawAdditional boolean Controls whether additional tokens should be drawn ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) end -- returns a Table List of chaos token ids in the current chaos bag @@ -535,50 +553,6 @@ do return ChaosBagApi end end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) __bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local MythosAreaApi = {} @@ -592,25 +566,77 @@ do MythosAreaApi.returnTokenData = function() return getMythosArea().call("returnTokenData") end - + ---@return any: Object reference to the encounter deck MythosAreaApi.getEncounterDeck = function() return getMythosArea().call("getEncounterDeck") end - -- draw an encounter card for the requesting mat - ---@param mat tts__Object Playermat that triggered this - ---@param alwaysFaceUp boolean Whether the card should be drawn face-up - MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp) - getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp}) + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) end -- reshuffle the encounter deck MythosAreaApi.reshuffleEncounterDeck = function() getMythosArea().call("reshuffleEncounterDeck") end - + return MythosAreaApi end end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi +end +end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Custom_Token Token Arranger 022907.yaml b/unpacked/Custom_Token Token Arranger 022907.yaml index d07dcbd99..23671a1a4 100644 --- a/unpacked/Custom_Token Token Arranger 022907.yaml +++ b/unpacked/Custom_Token Token Arranger 022907.yaml @@ -32,8 +32,8 @@ IgnoreFoW: false LayoutGroupSortIndex: 0 Locked: true LuaScript: !include 'Custom_Token Token Arranger 022907.ttslua' -LuaScriptState: '{"includeDrawnTokens":true,"percentage":false,"tokenPrecedence":{"":[0,11],"Auto-fail":[-100,7],"Bless":[101,8],"Cultist":[-2,4],"Curse":[-101,9],"Elder - Sign":[100,2],"Elder Thing":[-4,6],"Frost":[-99,10],"Skull":[-1,3],"Tablet":[-3,5]}}' +LuaScriptState: '{"includeDrawnTokens":true,"percentage":false,"tokenPrecedence":{"":[0,11],"Auto-fail":[-100,7],"Bless":[110,8],"Cultist":[-2,4],"Curse":[-110,9],"Elder + Sign":[100,2],"Elder Thing":[-4,6],"Frost":[-105,10],"Skull":[-1,3],"Tablet":[-3,5]}}' MeasureMovement: false Name: Custom_Token Nickname: Token Arranger diff --git a/unpacked/Custom_Token Victory Display 6ccd6d.ttslua b/unpacked/Custom_Token Victory Display 6ccd6d.ttslua index ba1591198..eddcba04a 100644 --- a/unpacked/Custom_Token Victory Display 6ccd6d.ttslua +++ b/unpacked/Custom_Token Victory Display 6ccd6d.ttslua @@ -41,9 +41,6 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/VictoryDisplay") -end) __bundle_register("chaosbag/ChaosBagApi", function(require, _LOADED, __bundle_register, __bundle_modules) do local ChaosBagApi = {} @@ -51,7 +48,7 @@ do -- respawns the chaos bag with a new state of tokens ---@param tokenList table List of chaos token ids ChaosBagApi.setChaosBagState = function(tokenList) - return Global.call("setChaosBagState", tokenList) + Global.call("setChaosBagState", tokenList) end -- returns a Table List of chaos token ids in the current chaos bag @@ -78,48 +75,57 @@ do -- returns all sealed tokens on cards to the chaos bag ---@param playerColor string Color of the player to show the broadcast to ChaosBagApi.releaseAllSealedTokens = function(playerColor) - return Global.call("releaseAllSealedTokens", playerColor) + Global.call("releaseAllSealedTokens", playerColor) end -- returns all drawn tokens to the chaos bag ChaosBagApi.returnChaosTokens = function() - return Global.call("returnChaosTokens") + Global.call("returnChaosTokens") end -- removes the specified chaos token from the chaos bag ---@param id string ID of the chaos token ChaosBagApi.removeChaosToken = function(id) - return Global.call("removeChaosToken", id) + Global.call("removeChaosToken", id) end -- returns a chaos token to the bag and calls all relevant functions ---@param token tts__Object Chaos token to return - ChaosBagApi.returnChaosTokenToBag = function(token) - return Global.call("returnChaosTokenToBag", token) + ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false) + ChaosBagApi.returnChaosTokenToBag = function(token, fromBag) + Global.call("returnChaosTokenToBag", { token = token, fromBag = fromBag }) end -- spawns the specified chaos token and puts it into the chaos bag ---@param id string ID of the chaos token ChaosBagApi.spawnChaosToken = function(id) - return Global.call("spawnChaosToken", id) + Global.call("spawnChaosToken", id) end -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the -- contents of the bag should check this method before doing so. -- This method will broadcast a message to all players if the bag is being searched. - ---@return any canTouch True if the bag is manipulated, false if it should be blocked. + ---@return any: True if the bag is manipulated, false if it should be blocked. ChaosBagApi.canTouchChaosTokens = function() return Global.call("canTouchChaosTokens") end - -- called by playermats (by the "Draw chaos token" button) + -- draws a chaos token to a playermat ---@param mat tts__Object Playermat that triggered this ---@param drawAdditional boolean Controls whether additional tokens should be drawn ---@param tokenType? string Name of token (e.g. "Bless") to be drawn from the bag ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag - ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved) - return Global.call("drawChaosToken", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved}) + ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token + ---@return tts__Object: Object reference to the token that was drawn + ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters) + return Global.call("drawChaosToken", { + mat = mat, + drawAdditional = drawAdditional, + tokenType = tokenType, + guidToBeResolved = guidToBeResolved, + takeParameters = takeParameters + }) end -- returns a Table List of chaos token ids in the current chaos bag @@ -139,6 +145,7 @@ do return getObjectFromGUID("123456") end + -- Returns the matching object ---@param owner string Parent object for this search ---@param type string Type of object to search for ---@return any: Object reference to the matching object @@ -146,21 +153,21 @@ do return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param type string Type of object to search for ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByType = function(type) return getGuidHandler().call("getObjectsByType", type) end - -- returns all matching objects as a table with references + -- Returns all matching objects as a table with references ---@param owner string Parent object for this search ---@return table: List of object references to matching objects GUIDReferenceApi.getObjectsByOwner = function(owner) return getGuidHandler().call("getObjectsByOwner", owner) end - -- sends new information to the reference handler to edit the main index + -- Sends new information to the reference handler to edit the main index ---@param owner string Parent of the object ---@param type string Type of the object ---@param guid string GUID of the object @@ -172,6 +179,13 @@ do }) end + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + return GUIDReferenceApi end end) @@ -188,13 +202,13 @@ do return guidReferenceApi.getObjectByOwnerAndType("Mythos", "InvestigatorCounter") end - -- Returns the current value of the investigator counter from the playmat + -- Returns the current value of the investigator counter from the playermat ---@return number: Number of investigators currently set on the counter PlayAreaApi.getInvestigatorCount = function() return getInvestigatorCounter().getVar("val") end - -- Updates the current value of the investigator counter from the playmat + -- Updates the current value of the investigator counter from the playermat ---@param count number Number of investigators to set on the counter PlayAreaApi.setInvestigatorCount = function(count) getInvestigatorCounter().call("updateVal", count) @@ -235,7 +249,7 @@ do getPlayArea().call("onScenarioChanged", scenarioName) end - -- Sets this playmat's snap points to limit snapping to locations or not. + -- Sets this playermat's snap points to limit snapping to locations or not. -- If matchTypes is false, snap points will be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types PlayAreaApi.setLimitSnapsByType = function(matchCardTypes) @@ -248,18 +262,18 @@ do getPlayArea().call("tryObjectEnterContainer", { container = container, object = object }) end - -- counts the VP on locations in the play area + -- Counts the VP on locations in the play area PlayAreaApi.countVP = function() return getPlayArea().call("countVP") end - -- highlights all locations in the play area without metadata + -- Highlights all locations in the play area without metadata ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightMissingData = function(state) return getPlayArea().call("highlightMissingData", state) end - - -- highlights all locations in the play area with VP + + -- Highlights all locations in the play area with VP ---@param state boolean True if highlighting should be enabled PlayAreaApi.highlightCountedVP = function(state) return getPlayArea().call("countVP", state) @@ -270,15 +284,26 @@ do return getPlayArea().call("isInPlayArea", object) end + -- Returns the current surface of the play area PlayAreaApi.getSurface = function() return getPlayArea().getCustomObject().image end + -- Updates the surface of the play area PlayAreaApi.updateSurface = function(url) return getPlayArea().call("updateSurface", url) end - - -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the + + -- Returns a deep copy of the currently tracked locations + PlayAreaApi.getTrackedLocations = function() + local t = {} + for k, v in pairs(getPlayArea().call("getTrackedLocations", {})) do + t[k] = v + end + return t + end + + -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the -- data to the local token manager instance. ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call PlayAreaApi.updateLocations = function(args) @@ -292,11 +317,202 @@ do return PlayAreaApi end end) +__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local CHAOS_TOKEN_NAMES = { + ["Elder Sign"] = true, + ["+1"] = true, + ["0"] = true, + ["-1"] = true, + ["-2"] = true, + ["-3"] = true, + ["-4"] = true, + ["-5"] = true, + ["-6"] = true, + ["-7"] = true, + ["-8"] = true, + ["Skull"] = true, + ["Cultist"] = true, + ["Tablet"] = true, + ["Elder Thing"] = true, + ["Auto-fail"] = true, + ["Bless"] = true, + ["Curse"] = true, + ["Frost"] = true + } + + local TokenChecker = {} + + -- returns true if the passed object is a chaos token (by name) + TokenChecker.isChaosToken = function(obj) + if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then + return true + else + return false + end + end + + return TokenChecker +end +end) +__bundle_register("util/DeckLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local DeckLib = {} + local searchLib = require("util/SearchLib") + + -- places a card/deck at a position or merges into an existing deck below + ---@param objOrTable tts__Object|table Object or table of objects to move + ---@param pos table New position for the object + ---@param rot? table New rotation for the object + ---@param below? boolean Should the object be placed below an existing deck? + DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below) + if objOrTable == nil or pos == nil then return end + + -- handle 'objOrTable' parameter + local objects = {} + if type(objOrTable) == "table" then + objects = objOrTable + else + table.insert(objects, objOrTable) + end + + -- search the new position for existing card/deck + local searchResult = searchLib.atPosition(pos, "isCardOrDeck") + local targetObj + + -- get new position + local offset = 0.5 + local newPos = Vector(pos) + Vector(0, offset, 0) + + if #searchResult == 1 then + targetObj = searchResult[1] + local bounds = targetObj.getBounds() + if below then + newPos = Vector(pos):setAt("y", bounds.center.y - bounds.size.y / 2) + else + newPos = Vector(pos):setAt("y", bounds.center.y + bounds.size.y / 2 + offset) + end + end + + -- process objects in reverse order + for i = #objects, 1, -1 do + local obj = objects[i] + -- add a 0.1 delay for each object (for animation purposes) + Wait.time(function() + -- allow moving smoothly out of hand and temporarily lock it + obj.setLock(true) + obj.use_hands = false + + if rot then + obj.setRotationSmooth(rot, false, true) + end + obj.setPositionSmooth(newPos, false, true) + + -- wait for object to finish movement (or 2 seconds) + Wait.condition( + function() + -- revert toggles + obj.setLock(false) + obj.use_hands = true + + -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting + if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then + targetObj = targetObj.putObject(obj) + else + targetObj = obj + end + end, + -- check state of the object (make sure it's not moving) + function() return obj.isDestroyed() or not obj.isSmoothMoving() end, + 2) + end, (#objects- i) * 0.1) + end + end + + return DeckLib +end +end) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } + + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList + end + + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) + end + + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib +end +end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/VictoryDisplay") +end) __bundle_register("core/VictoryDisplay", function(require, _LOADED, __bundle_register, __bundle_modules) -local searchLib = require("util/SearchLib") local chaosBagApi = require("chaosbag/ChaosBagApi") +local deckLib = require("util/DeckLib") local guidReferenceApi = require("core/GUIDReferenceApi") local playAreaApi = require("core/PlayAreaApi") +local searchLib = require("util/SearchLib") local tokenChecker = require("core/token/TokenChecker") local pendingCall = false @@ -529,28 +745,30 @@ end -- places the provided card in the first empty spot function placeCard(card) - local trash = guidReferenceApi.getObjectByOwnerAndType("Mythos", "Trash") + local name = card.getName() or "Unnamed card" - -- check snap point states + -- get sorted list of snap points to check slots local snaps = self.getSnapPoints() table.sort(snaps, function(a, b) return a.position.x > b.position.x end) table.sort(snaps, function(a, b) return a.position.z < b.position.z end) -- get first empty slot - local fullSlots = {} - local positions = {} + local emptyIndex, emptyPos for i, snap in ipairs(snaps) do - positions[i] = self.positionToWorld(snap.position) - local searchResult = searchLib.atPosition(positions[i], "isCardOrDeck") - fullSlots[i] = #searchResult > 0 + local snapPos = self.positionToWorld(snap.position) + local searchResult = searchLib.atPosition(snapPos, "isCardOrDeck") + if #searchResult == 0 then + emptyPos = snapPos + emptyIndex = i + break + end end -- remove tokens from the card - for _, obj in ipairs(searchLib.onObject(card)) do - -- don't touch decks / cards - if obj.type == "Deck" or obj.type == "Card" then + local trash = guidReferenceApi.getObjectByOwnerAndType("Mythos", "Trash") + for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do + if tokenChecker.isChaosToken(obj) then -- put chaos tokens back into bag - elseif tokenChecker.isChaosToken(obj) then local chaosBag = chaosBagApi.findChaosBag() chaosBag.putObject(obj) elseif obj.memo ~= nil and obj.getLock() == false then @@ -558,129 +776,17 @@ function placeCard(card) end end - -- place the card - local name = card.getName() or "Unnamed card" - for i = 1, 10 do - if fullSlots[i] ~= true then - local rot = { 0, 270, card.getRotation().z } - card.setPositionSmooth(positions[i], false, true) - card.setRotation(rot) - broadcastToAll("Victory Display: " .. name .. " placed into slot " .. i .. ".", "Green") - return - end + -- use the first snap position in case all slots are full + local pos = emptyPos or self.positionToWorld(snaps[1].position) + local rot = card.getRotation():setAt("x", 0):setAt("y", 270) + deckLib.placeOrMergeIntoDeck(card, pos, rot) + + -- give a feedback message + if emptyPos then + broadcastToAll("Victory Display: " .. name .. " placed into slot " .. emptyIndex .. ".", "Green") + else + broadcastToAll("Victory Display is full! " .. name .. " placed into slot 1.", "Orange") end - - broadcastToAll("Victory Display is full! " .. name .. " placed into slot 1.", "Orange") - card.setPositionSmooth(positions[1], false, true) -end -end) -__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local CHAOS_TOKEN_NAMES = { - ["Elder Sign"] = true, - ["+1"] = true, - ["0"] = true, - ["-1"] = true, - ["-2"] = true, - ["-3"] = true, - ["-4"] = true, - ["-5"] = true, - ["-6"] = true, - ["-7"] = true, - ["-8"] = true, - ["Skull"] = true, - ["Cultist"] = true, - ["Tablet"] = true, - ["Elder Thing"] = true, - ["Auto-fail"] = true, - ["Bless"] = true, - ["Curse"] = true, - ["Frost"] = true - } - - local TokenChecker = {} - - -- returns true if the passed object is a chaos token (by name) - TokenChecker.isChaosToken = function(obj) - if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then - return true - else - return false - end - end - - return TokenChecker -end -end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } - - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] - end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 - }) - - -- filtering the result - local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) - end - end - return objList - end - - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) - end - - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) - end - - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib end end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/Deck Tarot Deck 77f1e5.yaml b/unpacked/Deck Tarot Deck 77f1e5.yaml index 09ac99fac..da70d8252 100644 --- a/unpacked/Deck Tarot Deck 77f1e5.yaml +++ b/unpacked/Deck Tarot Deck 77f1e5.yaml @@ -55,11 +55,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -127,11 +128,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -199,11 +201,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -266,16 +269,17 @@ ContainedObjects: to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn - loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", - function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", + loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", + function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -343,11 +347,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -415,11 +420,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -487,11 +493,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -554,16 +561,17 @@ ContainedObjects: to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn - loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", - function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", + loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", + function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -631,11 +639,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -703,11 +712,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -775,11 +785,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -847,11 +858,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -919,11 +931,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -991,11 +1004,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1063,11 +1077,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1135,11 +1150,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1207,11 +1223,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1279,11 +1296,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1351,11 +1369,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1423,11 +1442,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1490,16 +1510,17 @@ ContainedObjects: to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn - loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", - function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", + loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", + function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card @@ -1567,11 +1588,12 @@ ContainedObjects: function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", - rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n - \ local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n - \ else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates - this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() - + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")" + rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n + \ Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n + \ if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle + = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n + \ self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn + __bundle_require(\"__root\")" LuaScriptState: '' MeasureMovement: false Name: Card diff --git a/unpacked/FogOfWarTrigger 3aab97.yaml b/unpacked/FogOfWarTrigger 3aab97.yaml index acc9ece09..307239aa0 100644 --- a/unpacked/FogOfWarTrigger 3aab97.yaml +++ b/unpacked/FogOfWarTrigger 3aab97.yaml @@ -4,7 +4,7 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - a: 0.25 + a: 0.75 b: 0.168 g: 0.701 r: 0.192 diff --git a/unpacked/FogOfWarTrigger Game Data 3dbe47.yaml b/unpacked/FogOfWarTrigger Game Data 3dbe47.yaml index dd53c1bfc..7090c0155 100644 --- a/unpacked/FogOfWarTrigger Game Data 3dbe47.yaml +++ b/unpacked/FogOfWarTrigger Game Data 3dbe47.yaml @@ -4,7 +4,7 @@ AltLookAngle: z: 0 Autoraise: true ColorDiffuse: - a: 0.25 + a: 0.75 b: 0.25 g: 0.25 r: 0.25 diff --git a/unpacked/Notecard d8d357.yaml b/unpacked/Notecard d8d357.yaml deleted file mode 100644 index f7b2ce581..000000000 --- a/unpacked/Notecard d8d357.yaml +++ /dev/null @@ -1,40 +0,0 @@ -AltLookAngle: - x: 0 - y: 0 - z: 0 -Autoraise: true -ColorDiffuse: - b: 1 - g: 1 - r: 1 -Description: lua setNotes(getObjectFromGUID('the objects guid').getJSON()) -DragSelectable: true -GMNotes: '' -GUID: d8d357 -Grid: true -GridProjection: false -Hands: false -HideWhenFaceDown: false -IgnoreFoW: false -LayoutGroupSortIndex: 0 -Locked: true -LuaScript: '' -LuaScriptState: '' -MeasureMovement: false -Name: Notecard -Nickname: '' -Snap: true -Sticky: true -Tooltip: false -Transform: - posX: 78 - posY: 1.24 - posZ: 33.58 - rotX: 0 - rotY: 90 - rotZ: 0 - scaleX: 0.25 - scaleY: 0.25 - scaleZ: 0.25 -Value: 0 -XmlUI: '' diff --git a/unpacked/Notecard Arkham SCE 3.7.0 - 352024 - Page 1 6657b6.yaml b/unpacked/Notecard Arkham SCE 3.7.0 - 352024 - Page 1 6657b6.yaml deleted file mode 100644 index 07394e4d2..000000000 --- a/unpacked/Notecard Arkham SCE 3.7.0 - 352024 - Page 1 6657b6.yaml +++ /dev/null @@ -1,145 +0,0 @@ -AltLookAngle: - x: 0 - y: 0 - z: 0 -Autoraise: true -ColorDiffuse: - b: 1 - g: 1 - r: 1 -Description: 'Thanks for downloading Arkham SCE 3.7.0! - - - - Feast of Hemlock Vale Campaign Expansion is now fully released! - - - Updated all FoHV player cards with high-quality scans! - - - Also updated Edge of the Earth player cards! - - - Implemented cards from the latest Taboo list update.' -DragSelectable: true -GMNotes: '' -GUID: 6657b6 -Grid: true -GridProjection: false -Hands: false -HideWhenFaceDown: false -IgnoreFoW: false -LayoutGroupSortIndex: 0 -Locked: false -LuaScript: '' -LuaScriptState: '' -MeasureMovement: false -Name: Notecard -Nickname: Arkham SCE 3.7.0 - 3/5/2024 - Page 1 -Snap: true -States: - '2': - AltLookAngle: - x: 0 - y: 0 - z: 0 - Autoraise: true - ColorDiffuse: - b: 1 - g: 1 - r: 1 - Description: '- Added a new card to the Fan-made Accessories barrel that can be - used to easily track bonus VP that can''t immediately be spent. - - - The "Take Clue" hotkey can now be used to take a clue from a clue pool bag, - and has variants for multi-handed play. - - - Opening a menu (such as the downloads menu) no longer causes it to appear - on every other player''s screen. - - ' - DragSelectable: true - GMNotes: '' - GUID: a22c7e - Grid: true - GridProjection: false - Hands: false - HideWhenFaceDown: false - IgnoreFoW: false - LayoutGroupSortIndex: 0 - Locked: false - LuaScript: '' - LuaScriptState: '' - MeasureMovement: false - Name: Notecard - Nickname: Arkham SCE 3.7.0 - 3/5/2024 - Page 2 - Snap: true - Sticky: true - Tooltip: true - Transform: - posX: 8.080297 - posY: 1.556635 - posZ: -35.1032448 - rotX: 8.872076e-05 - rotY: 89.97568 - rotZ: 0.0284017846 - scaleX: 3 - scaleY: 1 - scaleZ: 3 - Value: 0 - XmlUI: '' - '3': - AltLookAngle: - x: 0 - y: 0 - z: 0 - Autoraise: true - ColorDiffuse: - b: 1 - g: 1 - r: 1 - Description: '- Implemented many miscellaneous bugfixes and metadata tweaks. - - - - Extra-special thanks to everyone who helped us release Feast of Hemlock Vale - as quickly as we could!!' - DragSelectable: true - GMNotes: '' - GUID: c01be8 - Grid: true - GridProjection: false - Hands: false - HideWhenFaceDown: false - IgnoreFoW: false - LayoutGroupSortIndex: 0 - Locked: false - LuaScript: '' - LuaScriptState: '' - MeasureMovement: false - Name: Notecard - Nickname: Arkham SCE 3.7.0 - 3/5/2024 - Page 3 - Snap: true - Sticky: true - Tooltip: true - Transform: - posX: 48.5480232 - posY: 1.55149889 - posZ: 2.98858 - rotX: -8.131164e-08 - rotY: 90.00001 - rotZ: 5.652705e-08 - scaleX: 3 - scaleY: 1 - scaleZ: 3 - Value: 0 - XmlUI: '' -Sticky: true -Tooltip: true -Transform: - posX: -27 - posY: 1.55 - posZ: -56.16 - rotX: 0 - rotY: 90 - rotZ: 0 - scaleX: 3 - scaleY: 1 - scaleZ: 3 -Value: 0 -XmlUI: '' diff --git a/unpacked/go_game_piece_black Navigation Overlay Handler 797ede.ttslua b/unpacked/go_game_piece_black Navigation Overlay Handler 797ede.ttslua index aed998f02..3c8e399cd 100644 --- a/unpacked/go_game_piece_black Navigation Overlay Handler 797ede.ttslua +++ b/unpacked/go_game_piece_black Navigation Overlay Handler 797ede.ttslua @@ -41,55 +41,83 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } + + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList + end + + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) + end + + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib +end +end) __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) require("core/NavigationOverlayHandler") end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) __bundle_register("core/NavigationOverlayHandler", function(require, _LOADED, __bundle_register, __bundle_modules) -local playmatApi = require("playermat/PlaymatApi") +local playermatApi = require("playermat/PlayermatApi") fullButtonData = { { id = "1", width = "84", height = "33", offset = "1 2" }, -- 1. Act/Agenda @@ -135,10 +163,10 @@ playButtonData = { cameraData = { { position = { -1.6, 1.55, 0 }, distance = 18 }, -- 1. Act/Agenda { position = { -28, 1.55, 0 }, distance = -1 }, -- 2. Map - { position = { -31.6, 1.55, 26.4 }, distance = -1 }, -- 3. Green playmat - { position = { -55, 1.55, 12.05 }, distance = -1 }, -- 4. White playmat - { position = { -55, 1.55, -11.48 }, distance = -1 }, -- 5. Orange playmat - { position = { -31.6, 1.55, -26.4 }, distance = -1 }, -- 6. Red playmat + { position = { -31.6, 1.55, 26.4 }, distance = -1 }, -- 3. Green playermat + { position = { -55, 1.55, 12.05 }, distance = -1 }, -- 4. White playermat + { position = { -55, 1.55, -11.48 }, distance = -1 }, -- 5. Orange playermat + { position = { -31.6, 1.55, -26.4 }, distance = -1 }, -- 6. Red playermat { position = { -3, 1.55, 30 }, distance = 16 }, -- 7. Victory / SetAside { position = { -3, 1.55, -26.76 }, distance = 16 }, -- 8. Guide { position = { -11.83, 1.55, 0 }, distance = 10 }, -- 9. Player count @@ -173,20 +201,16 @@ function onSave() end function onLoad(savedData) - if savedData ~= "" then + if savedData and savedData ~= "" then local loadedData = JSON.decode(savedData) visibility = loadedData.visibility claims = loadedData.claims pitch = loadedData.pitch distance = loadedData.distance else - local allColors = Player.getColors() - - for _, color in ipairs(allColors) do - -- default state for claims + -- initialize tables with defaults + for _, color in ipairs(Player.getColors()) do claims[color] = {} - - -- default state for visibility visibility[color] = { full = false, play = false } end end @@ -280,7 +304,6 @@ end -- XML button creation function createXmlButtonHelper(ui, params) - local color local guid = self.getGUID() local xml = findTagWithId(ui, params.id) @@ -429,20 +452,20 @@ function loadCamera(player, camera) end end - -- swap to that color if it isn't claimed by someone else - if #getSeatedPlayers() == 1 or not isClaimed then - local newPlayerColor = playmatApi.getPlayerColor(matColor) + -- swap to that color if it isn't claimed by someone else and it's currently unoccopied + if #getSeatedPlayers() == 1 or (not isClaimed and isPlayermatAvailable(matColor)) then + local newPlayerColor = playermatApi.getPlayerColor(matColor) copyVisibility({ startColor = player.color, targetColor = newPlayerColor }) player.changeColor(newPlayerColor) player = Player[newPlayerColor] end - -- search on the playmat for objects - local bounds = getDynamicViewBounds(playmatApi.searchAroundPlaymat(matColor)) + -- search on the playermat for objects + local bounds = getDynamicViewBounds(playermatApi.searchAroundPlayermat(matColor)) lookHere = { position = { bounds.middleX, 0, bounds.middleZ }, - yaw = playmatApi.returnRotation(matColor).y + 180, + yaw = playermatApi.returnRotation(matColor).y + 180, distance = 0.42 * math.max(bounds.diffX, bounds.diffZ) + 7 } end @@ -463,6 +486,17 @@ function loadCamera(player, camera) Wait.frames(function() player.lookAt(lookHere) end, 2) end +-- helper function to check if a playermat is available for a color swap +function isPlayermatAvailable(matColor) + local newPlayerColor = playermatApi.getPlayerColor(matColor) + for _, color in ipairs(getSeatedPlayers()) do + if color == newPlayerColor then + return false + end + end + return true +end + --------------------------------------------------------- -- settings related functionality --------------------------------------------------------- @@ -526,15 +560,16 @@ function updateSettingsUI(player) end end end) -__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlaymatApi = {} + local PlayermatApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } -- Convenience function to look up a mat's object by color, or get all mats. - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@return table: Single-element if only single playmat is requested + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested local function getMatForColor(matColor) if matColor == "All" then return guidReferenceApi.getObjectsByType("Playermat") @@ -543,9 +578,9 @@ do end end - -- Returns the color of the closest playmat + -- Returns the color of the closest playermat ---@param startPos table Starting position to get the closest mat from - PlaymatApi.getMatColorByPosition = function(startPos) + PlayermatApi.getMatColorByPosition = function(startPos) local result, smallestDistance for matColor, mat in pairs(getMatForColor("All")) do local distance = Vector.between(startPos, mat.getPosition()):magnitude() @@ -557,17 +592,17 @@ do return result end - -- Returns the color of the player's hand that is seated next to the playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getPlayerColor = function(matColor) + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("playerColor") end end - -- Returns the color of the playmat that owns the playercolor's hand - ---@param handColor string Color of the playmat - PlaymatApi.getMatColor = function(handColor) + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) for matColor, mat in pairs(getMatForColor("All")) do local playerColor = mat.getVar("playerColor") if playerColor == handColor then @@ -576,59 +611,95 @@ do end end - -- Returns if there is the card "Dream-Enhancing Serum" on the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.isDES = function(matColor) + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("isDES") + mat.call("checkForDES") end end - -- Performs a search of the deck area of the requested playmat and returns the result as table - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDeckAreaObjects = function(matColor) + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getDeckAreaObjects") end end -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.flipTopCardFromDeck = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("flipTopCardFromDeck") end end - -- Returns the position of the discard pile of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDiscardPosition = function(matColor) + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("returnGlobalDiscardPosition") end end + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + -- Transforms a local position into a global position ---@param localPos table Local position to be transformed - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.transformLocalPosition = function(localPos, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.positionToWorld(localPos) end end - -- Returns the rotation of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.returnRotation = function(matColor) + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getRotation() end end -- Returns a table with spawn data (position and rotation) for a helper object - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param helperName string Name of the helper object - PlaymatApi.getHelperSpawnData = function(matColor, helperName) + PlayermatApi.getHelperSpawnData = function(matColor, helperName) local resultTable = {} local localPositionTable = { ["Hand Helper"] = {0.05, 0, -1.182}, @@ -645,73 +716,99 @@ do end - -- Triggers the Upkeep for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param playerColor string Color of the calling player (for messages) - PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doUpkeepFromHotkey", playerColor) 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") + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param objList table List of objects to discard - PlaymatApi.discardListOfObjects = function(matColor, objList) + PlayermatApi.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) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("activeInvestigatorId") end end - -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If -- matchTypes is true, the main card slot snap points will only snap assets, while the -- investigator area point will only snap Investigators. If matchTypes is false, snap points will -- be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("setLimitSnapsByType", matchCardTypes) end end - -- Sets the requested playmat's draw 1 button to visible + -- Sets the requested playermat's draw 1 button to visible ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("showDrawButton", isDrawButtonVisible) end end - -- Shows or hides the clickable clue counter for the requested playmat + -- Shows or hides the clickable clue counter for the requested playermat ---@param showCounter boolean Whether the clickable counter should be present or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.clickableClues = function(showCounter, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("clickableClues", showCounter) end end - -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.removeClues = function(matColor) + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("removeClues") end end - -- Reports the clue count for the requested playmat + -- Reports the clue count for the requested playermat ---@param useClickableCounters boolean Controls which type of counter is getting checked - PlaymatApi.getClueCount = function(useClickableCounters, matColor) + PlayermatApi.getClueCount = function(useClickableCounters, matColor) local count = 0 for _, mat in pairs(getMatForColor(matColor)) do count = count + mat.call("getClueCount", useClickableCounters) @@ -719,44 +816,41 @@ do return count end - -- updates the specified owned counter - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param type string Counter to target ---@param newValue number Value to set the counter to ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier - PlaymatApi.updateCounter = function(matColor, type, newValue, modifier) + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) for _, mat in pairs(getMatForColor(matColor)) do mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) end end - -- triggers the draw function for the specified playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param number number Amount of cards to draw - PlaymatApi.drawCardsWithReshuffle = function(matColor, number) + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) for _, mat in pairs(getMatForColor(matColor)) do mat.call("drawCardsWithReshuffle", number) end end - -- returns the resource counter amount - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param type string Counter to target - PlaymatApi.getCounterValue = function(matColor, type) + PlayermatApi.getCounterValue = function(matColor, type) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getCounterValue", type) end end - -- returns a list of mat colors that have an investigator placed - PlaymatApi.getUsedMatColors = function() - local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() local usedColors = {} - for matColor, mat in pairs(getMatForColor("All")) do local searchPos = mat.positionToWorld(localInvestigatorPosition) local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") - if #searchResult > 0 then table.insert(usedColors, matColor) end @@ -764,18 +858,39 @@ do return usedColors end - -- resets the specified skill tracker to "1, 1, 1, 1" - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.resetSkillTracker = function(matColor) + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("resetSkillTracker") end end - -- finds all objects on the playmat and associated set aside zone and returns a table - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param filter string Name of the filte function (see util/SearchLib) - PlaymatApi.searchAroundPlaymat = function(matColor, filter) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} for _, mat in pairs(getMatForColor(matColor)) do for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do @@ -786,92 +901,73 @@ do end -- Discard a non-hidden card from the corresponding player's hand - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.doDiscardOne = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doDiscardOne") end end - -- Triggers the metadata sync for all playmats - PlaymatApi.syncAllCustomizableCards = function() + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() for _, mat in pairs(getMatForColor("All")) do mat.call("syncAllCustomizableCards") end end - return PlaymatApi + return PlayermatApi end end) -__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local SearchLib = {} - local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, - isCard = function(x) return x.type == "Card" end, - isDeck = function(x) return x.type == "Deck" end, - isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, - isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end - } + local GUIDReferenceApi = {} - -- performs the actual search and returns a filtered list of object references - ---@param pos tts__Vector Global position - ---@param rot? tts__Vector Global rotation - ---@param size table Size - ---@param filter? string Name of the filter function - ---@param direction? table Direction (positive is up) - ---@param maxDistance? number Distance for the cast - local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) - local filterFunc - if filter then - filterFunc = filterFunctions[filter] - end - local searchResult = Physics.cast({ - origin = pos, - direction = direction or { 0, 1, 0 }, - orientation = rot or { 0, 0, 0 }, - type = 3, - size = size, - max_distance = maxDistance or 0 + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid }) - - -- filtering the result - local objList = {} - for _, v in ipairs(searchResult) do - if not filter or filterFunc(v.hit_object) then - table.insert(objList, v.hit_object) - end - end - return objList end - -- searches the specified area - SearchLib.inArea = function(pos, rot, size, filter) - return returnSearchResult(pos, rot, size, filter) + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) end - -- searches the area on an object - SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) - return returnSearchResult(pos, _, size, filter) - end - - -- searches the specified position (a single point) - SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } - return returnSearchResult(pos, _, size, filter) - end - - -- searches below the specified position (downwards until y = 0) - SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y - return returnSearchResult(pos, _, size, filter, direction, maxDistance) - end - - return SearchLib + return GUIDReferenceApi end end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/go_game_piece_white GUID Reference Handler 123456.ttslua b/unpacked/go_game_piece_white GUID Reference Handler 123456.ttslua index 9768d8571..1b562b5b1 100644 --- a/unpacked/go_game_piece_white GUID Reference Handler 123456.ttslua +++ b/unpacked/go_game_piece_white GUID Reference Handler 123456.ttslua @@ -45,6 +45,8 @@ __bundle_register("__root", function(require, _LOADED, __bundle_register, __bund require("core/GUIDReferenceHandler") end) __bundle_register("core/GUIDReferenceHandler", function(require, _LOADED, __bundle_register, __bundle_modules) +local searchLib = require("util/SearchLib") + local GuidReferences = { White = { ClueCounter = "d86b7c", @@ -203,5 +205,122 @@ function editIndex(params) editsToIndex[params.owner][params.type] = params.guid updateMainIndex() end + +-- Returns the owner of the provided object (either the matColor or "Mythos") +---@param object tts__GameObject Object to check +function getOwnerOfObject(object) + if object == nil then return end + + -- use GUID to check owners instead of obtaining each as reference + local objectGuid = object.getGUID() + + -- check if object is directly owned + for owner, subtable in pairs(GuidReferences) do + for type, guid in pairs(subtable) do + if guid == objectGuid then + return owner + end + end + end + + -- check if the object is in a handzone + for owner, subtable in pairs(GuidReferences) do + for type, guid in pairs(subtable) do + for _, zone in ipairs(object.getZones()) do + if guid == zone.getGUID() then + return owner + end + end + end + end + + -- check if it is on an owned object + local result = searchLib.belowPosition(object.getPosition()) + + for owner, subtable in pairs(GuidReferences) do + for type, guid in pairs(subtable) do + for _, searchObj in ipairs(result) do + if guid == searchObj.getGUID() then + return owner + end + end + end + end + + -- default to "Mythos" + return "Mythos" +end +end) +__bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local SearchLib = {} + local filterFunctions = { + isCard = function(x) return x.type == "Card" end, + isDeck = function(x) return x.type == "Deck" end, + isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, + isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, + } + + -- performs the actual search and returns a filtered list of object references + ---@param pos tts__Vector Global position + ---@param rot? tts__Vector Global rotation + ---@param size table Size + ---@param filter? string Name of the filter function + ---@param direction? table Direction (positive is up) + ---@param maxDistance? number Distance for the cast + local function returnSearchResult(pos, rot, size, filter, direction, maxDistance) + local filterFunc + if filter then + filterFunc = filterFunctions[filter] + end + local searchResult = Physics.cast({ + origin = pos, + direction = direction or { 0, 1, 0 }, + orientation = rot or { 0, 0, 0 }, + type = 3, + size = size, + max_distance = maxDistance or 0 + }) + + -- filter the result for matching objects + local objList = {} + for _, v in ipairs(searchResult) do + if not filter or filterFunc(v.hit_object) then + table.insert(objList, v.hit_object) + end + end + return objList + end + + -- searches the specified area + SearchLib.inArea = function(pos, rot, size, filter) + return returnSearchResult(pos, rot, size, filter) + end + + -- searches the area on an object + SearchLib.onObject = function(obj, filter) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) + return returnSearchResult(pos, _, size, filter) + end + + -- searches the specified position (a single point) + SearchLib.atPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + return returnSearchResult(pos, _, size, filter) + end + + -- searches below the specified position (downwards until y = 0) + SearchLib.belowPosition = function(pos, filter) + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y + return returnSearchResult(pos, _, size, filter, direction, maxDistance) + end + + return SearchLib +end end) return __bundle_require("__root") \ No newline at end of file diff --git a/unpacked/go_game_piece_white Game Key Handler fce69c.ttslua b/unpacked/go_game_piece_white Game Key Handler fce69c.ttslua index 677ea6904..714788f0c 100644 --- a/unpacked/go_game_piece_white Game Key Handler fce69c.ttslua +++ b/unpacked/go_game_piece_white Game Key Handler fce69c.ttslua @@ -41,486 +41,38 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = ( return require, loaded, register, modules end)(nil) -__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) -require("core/GameKeyHandler") -end) -__bundle_register("chaosbag/BlessCurseManagerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/MythosAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules) do - local BlessCurseManagerApi = {} + local MythosAreaApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") - local function getManager() - return guidReferenceApi.getObjectByOwnerAndType("Mythos", "BlessCurseManager") + local function getMythosArea() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "MythosArea") end - -- removes all taken tokens and resets the counts - BlessCurseManagerApi.removeTakenTokensAndReset = function() - local BlessCurseManager = getManager() - Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Bless") end, 0.05) - Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Curse") end, 0.10) - Wait.time(function() BlessCurseManager.call("doReset", "White") end, 0.15) + ---@return any: Table of chaos token metadata (if provided through scenario reference card) + MythosAreaApi.returnTokenData = function() + return getMythosArea().call("returnTokenData") end - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.sealedToken = function(type, guid) - getManager().call("sealedToken", { type = type, guid = guid }) + ---@return any: Object reference to the encounter deck + MythosAreaApi.getEncounterDeck = function() + return getMythosArea().call("getEncounterDeck") end - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.releasedToken = function(type, guid) - getManager().call("releasedToken", { type = type, guid = guid }) + -- draw an encounter card for the requesting mat to the first empty spot from the right + ---@param matColor string Playermat that triggered this + ---@param position tts__Vector Position for the encounter card + MythosAreaApi.drawEncounterCard = function(matColor, position) + getMythosArea().call("drawEncounterCard", { matColor = matColor, position = position }) end - -- updates the internal count (called by cards that seal bless/curse tokens) - ---@param type string Type of chaos token ("Bless" or "Curse") - ---@param guid string GUID of the token - BlessCurseManagerApi.returnedToken = function(type, guid) - getManager().call("returnedToken", { type = type, guid = guid }) + -- reshuffle the encounter deck + MythosAreaApi.reshuffleEncounterDeck = function() + getMythosArea().call("reshuffleEncounterDeck") end - -- broadcasts the current status for bless/curse tokens - ---@param playerColor string Color of the player to show the broadcast to - BlessCurseManagerApi.broadcastStatus = function(playerColor) - getManager().call("broadcastStatus", playerColor) - end - - -- removes all bless / curse tokens from the chaos bag and play - ---@param playerColor string Color of the player to show the broadcast to - BlessCurseManagerApi.removeAll = function(playerColor) - getManager().call("doRemove", playerColor) - end - - -- adds bless / curse sealing to the hovered card - ---@param playerColor string Color of the player to show the broadcast to - ---@param hoveredObject tts__Object Hovered object - BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject) - getManager().call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject }) - end - - return BlessCurseManagerApi -end -end) -__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) -do - local GUIDReferenceApi = {} - - local function getGuidHandler() - return getObjectFromGUID("123456") - end - - ---@param owner string Parent object for this search - ---@param type string Type of object to search for - ---@return any: Object reference to the matching object - GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) - return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) - end - - -- returns all matching objects as a table with references - ---@param type string Type of object to search for - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByType = function(type) - return getGuidHandler().call("getObjectsByType", type) - end - - -- returns all matching objects as a table with references - ---@param owner string Parent object for this search - ---@return table: List of object references to matching objects - GUIDReferenceApi.getObjectsByOwner = function(owner) - return getGuidHandler().call("getObjectsByOwner", owner) - end - - -- sends new information to the reference handler to edit the main index - ---@param owner string Parent of the object - ---@param type string Type of the object - ---@param guid string GUID of the object - GUIDReferenceApi.editIndex = function(owner, type, guid) - return getGuidHandler().call("editIndex", { - owner = owner, - type = type, - guid = guid - }) - end - - return GUIDReferenceApi -end -end) -__bundle_register("core/GameKeyHandler", function(require, _LOADED, __bundle_register, __bundle_modules) -local blessCurseManagerApi = require("chaosbag/BlessCurseManagerApi") -local guidReferenceApi = require("core/GUIDReferenceApi") -local navigationOverlayApi = require("core/NavigationOverlayApi") -local optionPanelApi = require("core/OptionPanelApi") -local playmatApi = require("playermat/PlaymatApi") -local searchLib = require("util/SearchLib") -local victoryDisplayApi = require("core/VictoryDisplayApi") - -function onLoad() - addHotkey("Add doom to agenda", addDoomToAgenda) - addHotkey("Add Bless/Curse context menu", addBlurseSealingMenu) - addHotkey("Discard object", discardObject) - addHotkey("Discard top card", discardTopDeck) - addHotkey("Display Bless/Curse status", showBlessCurseStatus) - addHotkey("Move card to Victory Display", moveCardToVictoryDisplay) - addHotkey("Remove a use", removeOneUse) - addHotkey("Switch seat clockwise", switchSeatClockwise) - addHotkey("Switch seat counter-clockwise", switchSeatCounterClockwise) - addHotkey("Take clue from location", takeClueFromLocation) - addHotkey("Take clue from location (White)", takeClueFromLocationWhite) - addHotkey("Take clue from location (Orange)", takeClueFromLocationOrange) - addHotkey("Take clue from location (Green)", takeClueFromLocationGreen) - addHotkey("Take clue from location (Red)", takeClueFromLocationRed) - addHotkey("Upkeep", triggerUpkeep) - addHotkey("Upkeep (Multi-handed)", triggerUpkeepMultihanded) -end - --- triggers the "Upkeep" function of the calling player's playmat -function triggerUpkeep(playerColor) - if playerColor == "Black" then - broadcastToColor("Triggering 'Upkeep (Multihanded)' instead", playerColor, "Yellow") - triggerUpkeepMultihanded(playerColor) - return - end - local matColor = playmatApi.getMatColor(playerColor) - playmatApi.doUpkeepFromHotkey(matColor, playerColor) -end - --- triggers the "Upkeep" function of the calling player's playmat AND --- for all playmats that don't have a seated player, but a investigator card -function triggerUpkeepMultihanded(playerColor) - if playerColor ~= "Black" then - triggerUpkeep(playerColor) - end - local colors = Player.getAvailableColors() - for _, handColor in ipairs(colors) do - local matColor = playmatApi.getMatColor(handColor) - if playmatApi.returnInvestigatorId(matColor) ~= "00000" and Player[handColor].seated == false then - playmatApi.doUpkeepFromHotkey(matColor, playerColor) - end - end -end - --- adds 1 doom to the agenda -function addDoomToAgenda() - local doomCounter = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DoomCounter") - 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 _, obj in ipairs(searchLib.onObject(hoveredObject, "isTileOrToken")) do - table.insert(discardTheseObjects, obj) - end - end - - local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor) - playmatApi.discardListOfObjects(discardForMatColor, discardTheseObjects) -end - --- discard the top card of hovered deck, calling discardObject function -function discardTopDeck(playerColor, hoveredObject) - -- only continue if an unlocked card or deck was hovered - if hoveredObject == nil - or (hoveredObject.type ~= "Card" and hoveredObject.type ~= "Deck") - or hoveredObject.locked then - broadcastToColor("Hover a deck/card and try again.", playerColor, "Yellow") - return - end - if hoveredObject.type == "Deck" then - takenCard = hoveredObject.takeObject({index = 0}) - else - takenCard = hoveredObject - end - Wait.frames(function() discardObject(playerColor, takenCard) end, 1) -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 - elseif pos.y > (Player[playerColor].getHandTransform().position.y - (Player[playerColor].getHandTransform().scale.y / 2)) then -- discard to closest mat if card is in a hand - 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 _, obj in ipairs(searchLib.onObject(hoveredObject, "isTileOrToken")) do - if 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 - --- switches the triggering player to the next seat (clockwise) -function switchSeatClockwise(playerColor) - switchSeat(playerColor, "clockwise") -end - --- switches the triggering player to the next seat (counter-clockwise) -function switchSeatCounterClockwise(playerColor) - switchSeat(playerColor, "counter-clockwise") -end - --- handles seat switching in the given direction -function switchSeat(playerColor, direction) - if playerColor == "Black" or playerColor == "Grey" then - broadcastToColor("This hotkey is only available to seated players.", playerColor, "Orange") - return - end - - -- sort function for matcolors based on hand position (Green, White, Orange, Red) - local function sortByHandPosition(color1, color2) - local pos1 = Player[color1].getHandTransform().position - local pos2 = Player[color2].getHandTransform().position - return pos1.z > pos2.z - end - - -- get used playermats - local usedColors = playmatApi.getUsedMatColors() - table.sort(usedColors, sortByHandPosition) - - -- get current seat index - local index - for i, color in ipairs(usedColors) do - if color == playerColor then - index = i - break - end - end - if not index then - broadcastToColor("Couldn't detect investigator.", playerColor, "Orange") - return - end - - -- get next color - index = index + ((direction == "clockwise") and -1 or 1) - if index == 0 then - index = #usedColors - elseif index > #usedColors then - index = 1 - end - - -- swap color - navigationOverlayApi.loadCamera(Player[playerColor], usedColors[index]) -end - -function takeClueFromLocationWhite(_, hoveredObject) - takeClueFromLocation("White", hoveredObject) -end - -function takeClueFromLocationOrange(_, hoveredObject) - takeClueFromLocation("Orange", hoveredObject) -end - -function takeClueFromLocationGreen(_, hoveredObject) - takeClueFromLocation("Green", hoveredObject) -end - -function takeClueFromLocationRed(_, hoveredObject) - takeClueFromLocation("Red", hoveredObject) -end - --- takes a clue from a location, player needs to hover the clue directly or the location -function takeClueFromLocation(playerColor, hoveredObject) - local cardName, clue - - if hoveredObject == nil then - broadcastToColor("Hover a clue or card with clues and try again.", playerColor, "Yellow") - return - elseif hoveredObject.type == "Card" then - cardName = hoveredObject.getName() - local searchResult = searchLib.onObject(hoveredObject, "isClue") - - if #searchResult == 0 then - broadcastToColor("This card does not have any clues on it.", playerColor, "Yellow") - return - else - clue = searchResult[1] - end - elseif hoveredObject.memo == "clueDoom" then - if hoveredObject.is_face_down then - broadcastToColor("This is a doom token and not a clue.", playerColor, "Yellow") - return - end - - clue = hoveredObject - local searchResult = searchLib.belowPosition(clue.getPosition(), "isCard") - - if #searchResult ~= 0 then - cardName = searchResult[1].getName() - end - elseif hoveredObject.type == "Infinite" and hoveredObject.getName() == "Clue tokens" then - clue = hoveredObject.takeObject() - cardName = "token pool" - else - broadcastToColor("Hover a clue or card with clues and try again.", playerColor, "Yellow") - return - end - - local clickableClues = optionPanelApi.getOptions()["useClueClickers"] - local playerName = Player[playerColor].steam_name - local matColor = playmatApi.getMatColor(playerColor) - local pos = nil - if clickableClues then - pos = {x = 0.49, y = 2.66, z = 0.00} - playmatApi.updateCounter(matColor, "ClickableClueCounter", _, 1) - else - pos = playmatApi.transformLocalPosition({x = -1.12, y = 0.05, z = 0.7}, matColor) - end - - local rot = playmatApi.returnRotation(matColor) - - -- check if found clue is a stack or single token - if clue.getQuantity() > 1 then - clue.takeObject({position = pos, rotation = rot}) - else - clue.setPositionSmooth(pos) - clue.setRotation(rot) - end - - if cardName then - broadcastToAll(playerName .. " took one clue from " .. cardName .. ".", "White") - else - broadcastToAll(playerName .. " took one clue.", "White") - end - - victoryDisplayApi.update() -end - --- broadcasts the bless/curse status to the calling player -function showBlessCurseStatus(playerColor) - blessCurseManagerApi.broadcastStatus(playerColor) -end - --- adds Wendy's menu to the hovered card -function addBlurseSealingMenu(playerColor, hoveredObject) - blessCurseManagerApi.addBlurseSealingMenu(playerColor, hoveredObject) -end - --- Simple method to check if the given point is in a specified area ----@param point tts__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() + return MythosAreaApi end end) __bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules) @@ -605,15 +157,655 @@ do return VictoryDisplayApi end end) -__bundle_register("playermat/PlaymatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules) do - local PlaymatApi = {} + local CHAOS_TOKEN_NAMES = { + ["Elder Sign"] = true, + ["+1"] = true, + ["0"] = true, + ["-1"] = true, + ["-2"] = true, + ["-3"] = true, + ["-4"] = true, + ["-5"] = true, + ["-6"] = true, + ["-7"] = true, + ["-8"] = true, + ["Skull"] = true, + ["Cultist"] = true, + ["Tablet"] = true, + ["Elder Thing"] = true, + ["Auto-fail"] = true, + ["Bless"] = true, + ["Curse"] = true, + ["Frost"] = true + } + + local TokenChecker = {} + + -- returns true if the passed object is a chaos token (by name) + TokenChecker.isChaosToken = function(obj) + if obj.type == "Tile" and CHAOS_TOKEN_NAMES[obj.getName()] then + return true + else + return false + end + end + + return TokenChecker +end +end) +__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules) +require("core/GameKeyHandler") +end) +__bundle_register("core/GameKeyHandler", function(require, _LOADED, __bundle_register, __bundle_modules) +local blessCurseManagerApi = require("chaosbag/BlessCurseManagerApi") +local guidReferenceApi = require("core/GUIDReferenceApi") +local mythosAreaApi = require("core/MythosAreaApi") +local navigationOverlayApi = require("core/NavigationOverlayApi") +local optionPanelApi = require("core/OptionPanelApi") +local playermatApi = require("playermat/PlayermatApi") +local searchLib = require("util/SearchLib") +local tokenChecker = require("core/token/TokenChecker") +local victoryDisplayApi = require("core/VictoryDisplayApi") + +function onLoad() + addHotkey("Add doom to agenda", addDoomToAgenda) + addHotkey("Add Bless/Curse context menu", addBlurseSealingMenu) + addHotkey("Discard object", discardObject) + addHotkey("Discard top card", discardTopDeck) + addHotkey("Display Bless/Curse status", showBlessCurseStatus) + addHotkey("Move card to Victory Display", moveCardToVictoryDisplay) + addHotkey("Place card into threat area", takeCardIntoThreatArea) + addHotkey("Remove a use", removeOneUse) + addHotkey("Reshuffle encounter deck", mythosAreaApi.reshuffleEncounterDeck) + addHotkey("Switch seat clockwise", switchSeatClockwise) + addHotkey("Switch seat counter-clockwise", switchSeatCounterClockwise) + addHotkey("Take clue from location", takeClueFromLocation) + addHotkey("Take clue from location (White)", takeClueFromLocationWhite) + addHotkey("Take clue from location (Orange)", takeClueFromLocationOrange) + addHotkey("Take clue from location (Green)", takeClueFromLocationGreen) + addHotkey("Take clue from location (Red)", takeClueFromLocationRed) + addHotkey("Upkeep", triggerUpkeep) + addHotkey("Upkeep (Multi-handed)", triggerUpkeepMultihanded) +end + +-- triggers the "Upkeep" function of the calling player's playermat +function triggerUpkeep(playerColor) + if playerColor == "Black" then + broadcastToColor("Triggering 'Upkeep (Multihanded)' instead", playerColor, "Yellow") + triggerUpkeepMultihanded(playerColor) + return + end + local matColor = playermatApi.getMatColor(playerColor) + playermatApi.doUpkeepFromHotkey(matColor, playerColor) +end + +-- triggers the "Upkeep" function of the calling player's playermat AND +-- for all playermats that don't have a seated player, but an investigator card +function triggerUpkeepMultihanded(playerColor) + if playerColor ~= "Black" then + triggerUpkeep(playerColor) + end + local colors = Player.getAvailableColors() + for _, handColor in ipairs(colors) do + local matColor = playermatApi.getMatColor(handColor) + if playermatApi.returnInvestigatorId(matColor) ~= "00000" and Player[handColor].seated == false then + playermatApi.doUpkeepFromHotkey(matColor, playerColor) + end + end +end + +-- adds 1 doom to the agenda +function addDoomToAgenda() + local doomCounter = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DoomCounter") + doomCounter.call("addVal", 1) +end + +-- move the hovered object to the nearest empty slot on the playermat +function takeCardIntoThreatArea(playerColor, hoveredObject) + -- only continue if an unlocked card + if hoveredObject == nil + or hoveredObject.type ~= "Card" and hoveredObject.type ~= "Deck" + or hoveredObject.hasTag("Location") + or hoveredObject.locked then + broadcastToColor("Hover a non-location card and try again.", playerColor, "Yellow") + return + end + + local matColor = playermatApi.getMatColor(playerColor) + local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat") + + -- do not continue if the threat area is already full + local threatAreaPos = playermatApi.getEncounterCardDrawPosition(matColor, false) + if threatAreaPos == playermatApi.getEncounterCardDrawPosition(matColor, true) then + broadcastToColor("Threat area is full.", playerColor, "Yellow") + return + end + + -- initialize list of objects to move (and store local positions) + local additionalObjects = {} + for _, obj in ipairs(searchLib.onObject(hoveredObject, "isTileOrToken")) do + local data = {} + data.object = obj + data.localPos = hoveredObject.positionToLocal(obj.getPosition()) + table.insert(additionalObjects, data) + end + + -- find out if the original card is on the green or red playermats + local originalMatColor = guidReferenceApi.getOwnerOfObject(hoveredObject) + + -- determine modifiers for the playermats + local modifierY = 0 + if originalMatColor == "Red" then + modifierY = 90 + elseif originalMatColor == "Green" then + modifierY = -90 + end + + local cardName = hoveredObject.getName() + if cardName == "" then cardName = "card" end + broadcastToAll("Moved " .. cardName .. " to " .. getColoredName(playerColor) .. "'s threat area.", "White") + + -- get new rotation (rounded) + local cardRot = hoveredObject.getRotation() + local roundedRotY = roundToMultiple(cardRot.y, 45) + local deltaRotY = 270 - mat.getRotation().y - modifierY + local newCardRot = cardRot:setAt("y", roundedRotY - deltaRotY) + + -- move the main card to threat area + hoveredObject.setRotation(newCardRot) + hoveredObject.setPosition(threatAreaPos) + + -- move tokens/tiles (to new global position) + for _, data in ipairs(additionalObjects) do + if not data.object.locked then + data.object.setPosition(hoveredObject.positionToWorld(data.localPos)) + data.object.setRotation(data.object.getRotation() - Vector(0, deltaRotY, 0)) + end + end +end + +-- discard the hovered or selected objects to the respective trashcan and discard tokens on it if it was a card +function discardObject(playerColor, hoveredObject) + -- if more than one object is selected, discard them all, one at a time + local selectedObjects = Player[playerColor].getSelectedObjects() + if #selectedObjects > 0 then + discardGroup(playerColor, selectedObjects) + return + -- only continue if an unlocked card, deck or tile was hovered + elseif 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 + + -- These should probably not be discarded normally. Ask player for confirmation. + local tokenData = mythosAreaApi.returnTokenData() + local scenarioName = tokenData.currentScenario + if scenarioName ~= "Lost in Time and Space" and scenarioName ~= "The Secret Name" then + if hoveredObject.type == "Deck" or hoveredObject.hasTag("Location") then + local suspect = (hoveredObject.type == "Deck") and "Deck" or "Location" + Player[playerColor].showConfirmDialog("Discard " .. suspect .. "?", + function() performDiscard(playerColor, hoveredObject) end) + return + end + end + + performDiscard(playerColor, hoveredObject) +end + +-- actually performs the discarding of the object and tokens / tiles on it +function performDiscard(playerColor, hoveredObject) + -- initialize list of objects to discard + local discardTheseObjects = { hoveredObject } + + -- discard tokens / tiles on cards / decks + if hoveredObject.type ~= "Tile" then + for _, obj in ipairs(searchLib.onObject(hoveredObject, "isTileOrToken")) do + table.insert(discardTheseObjects, obj) + end + end + + local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor) + playermatApi.discardListOfObjects(discardForMatColor, discardTheseObjects) +end + +function discardGroup(playerColor, selectedObjects) + local count = #selectedObjects + -- discarding one at a time avoids an error with cards in the discard pile losing the 'hands' toggle and uses multiple mats + for i = count, 1, -1 do + Wait.time(function() + if (selectedObjects[i].type == "Card" or selectedObjects[i].type ~= "Deck" or selectedObjects[i].type == "Tile") then + performDiscard(playerColor, selectedObjects[i]) + end + end, (count - i + 1) * 0.1) + end +end + +-- discard the top card of hovered deck, calling discardObject function +function discardTopDeck(playerColor, hoveredObject) + -- only continue if an unlocked card or deck was hovered + if hoveredObject == nil + or (hoveredObject.type ~= "Card" and hoveredObject.type ~= "Deck") + or hoveredObject.locked then + broadcastToColor("Hover a deck/card and try again.", playerColor, "Yellow") + return + end + + -- take top card from deck (unless it is already a single card) + local takenCard = hoveredObject + if hoveredObject.type == "Deck" then + takenCard = hoveredObject.takeObject({ index = 0 }) + end + Wait.frames(function() performDiscard(playerColor, takenCard) end, 1) +end + +-- helper function to get the player to trigger the discard function for +function getColorToDiscardFor(hoveredObject, playerColor) + local pos = hoveredObject.getPosition() + local closestMatColor = playermatApi.getMatColorByPosition(pos) + + -- check if actually on the closest playermat + local closestMat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, "Playermat") + local bounds = closestMat.getBounds() + + -- define the area "near" the playermat + local bufferAroundPlayermat = 2 + local areaNearPlayermat = {} + areaNearPlayermat.minX = bounds.center.x - bounds.size.x / 2 - bufferAroundPlayermat + areaNearPlayermat.maxX = bounds.center.x + bounds.size.x / 2 + bufferAroundPlayermat + areaNearPlayermat.minZ = bounds.center.z - bounds.size.z / 2 - bufferAroundPlayermat + areaNearPlayermat.maxZ = bounds.center.z + bounds.size.z / 2 + bufferAroundPlayermat + + -- discard to closest mat if near it + if inArea(pos, areaNearPlayermat) then + return closestMatColor + end + + -- discard to closest mat if card is in a hand + local handZone = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, "HandZone") + for _, zone in ipairs(hoveredObject.getZones()) do + if zone == handZone then + return closestMatColor + end + end + + -- discard to triggering mat if previous conditions weren't met + return playermatApi.getMatColor(playerColor) +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 _, obj in ipairs(searchLib.onObject(hoveredObject, "isTileOrToken")) do + if not obj.locked and obj.memo ~= "resourceCounter" then + -- check for matching object, otherwise use the first hit + if obj.memo and obj.memo == searchForType then + targetObject = obj + break + elseif not targetObject then + targetObject = obj + end + end + end + end + + -- release sealed token if card has one and no uses + if tokenChecker.isChaosToken(targetObject) and hoveredObject.hasTag("CardThatSeals") then + local func = hoveredObject.getVar("releaseOneToken") -- check if function exists + if func ~= nil then + hoveredObject.call("releaseOneToken", playerColor) + return + 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 + + broadcastToAll(getColoredName(playerColor) .. " removed a token: " .. tokenName, playerColor) + + local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor) + playermatApi.discardListOfObjects(discardForMatColor, { targetObject }) +end + +-- switches the triggering player to the next seat (clockwise) +function switchSeatClockwise(playerColor) + switchSeat(playerColor, "clockwise") +end + +-- switches the triggering player to the next seat (counter-clockwise) +function switchSeatCounterClockwise(playerColor) + switchSeat(playerColor, "counter-clockwise") +end + +-- handles seat switching in the given direction +function switchSeat(playerColor, direction) + if playerColor == "Black" or playerColor == "Grey" then + broadcastToColor("This hotkey is only available to seated players.", playerColor, "Orange") + return + end + + -- sort function for matcolors based on hand position (Green, White, Orange, Red) + local function sortByHandPosition(color1, color2) + local pos1 = Player[color1].getHandTransform().position + local pos2 = Player[color2].getHandTransform().position + return pos1.z > pos2.z + end + + -- get used playermats + local usedColors = playermatApi.getUsedMatColors() + table.sort(usedColors, sortByHandPosition) + + -- get current seat index + local index + for i, color in ipairs(usedColors) do + if color == playerColor then + index = i + break + end + end + if not index then + broadcastToColor("Couldn't detect investigator.", playerColor, "Orange") + return + end + + -- get next color + index = index + ((direction == "clockwise") and -1 or 1) + if index == 0 then + index = #usedColors + elseif index > #usedColors then + index = 1 + end + + -- swap color + navigationOverlayApi.loadCamera(Player[playerColor], usedColors[index]) +end + +function takeClueFromLocationWhite(_, hoveredObject) + takeClueFromLocation("White", hoveredObject) +end + +function takeClueFromLocationOrange(_, hoveredObject) + takeClueFromLocation("Orange", hoveredObject) +end + +function takeClueFromLocationGreen(_, hoveredObject) + takeClueFromLocation("Green", hoveredObject) +end + +function takeClueFromLocationRed(_, hoveredObject) + takeClueFromLocation("Red", hoveredObject) +end + +-- takes a clue from a location, player needs to hover the clue directly or the location +function takeClueFromLocation(playerColor, hoveredObject) + -- use different color for messages if player is not seated (because this hotkey is called for a different mat) + local messageColor = playerColor + if not Player[playerColor] or not Player[playerColor].seated then + messageColor = getFirstSeatedPlayer() + end + + local cardName, clue + + if hoveredObject == nil then + broadcastToColor("Hover a clue or card with clues and try again.", messageColor, "Yellow") + return + elseif hoveredObject.type == "Card" then + cardName = hoveredObject.getName() + local searchResult = searchLib.onObject(hoveredObject, "isClue") + + if #searchResult == 0 then + broadcastToColor("This card does not have any clues on it.", messageColor, "Yellow") + return + else + clue = searchResult[1] + end + elseif hoveredObject.memo == "clueDoom" then + if hoveredObject.is_face_down then + broadcastToColor("This is a doom token and not a clue.", messageColor, "Yellow") + return + end + + clue = hoveredObject + local searchResult = searchLib.belowPosition(clue.getPosition(), "isCard") + + if #searchResult ~= 0 then + cardName = searchResult[1].getName() + end + elseif hoveredObject.type == "Infinite" and hoveredObject.getName() == "Clue tokens" then + clue = hoveredObject.takeObject() + cardName = "token pool" + else + broadcastToColor("Hover a clue or card with clues and try again.", messageColor, "Yellow") + return + end + + local clickableClues = optionPanelApi.getOptions()["useClueClickers"] + + -- handling for calling this for a specific mat via hotkey + local matColor, pos + if Player[playerColor] and Player[playerColor].seated then + matColor = playermatApi.getMatColor(playerColor) + else + matColor = playerColor + end + + if clickableClues then + pos = { x = 0.49, y = 2.66, z = 0.00 } + playermatApi.updateCounter(matColor, "ClickableClueCounter", _, 1) + else + pos = playermatApi.transformLocalPosition({ x = -1.12, y = 0.05, z = 0.7 }, matColor) + end + + local rot = playermatApi.returnRotation(matColor) + + -- check if found clue is a stack or single token + if clue.getQuantity() > 1 then + clue.takeObject({ position = pos, rotation = rot }) + else + clue.setPositionSmooth(pos) + clue.setRotation(rot) + end + + if cardName then + broadcastToAll(getColoredName(playerColor) .. " took one clue from " .. cardName .. ".", "White") + else + broadcastToAll(getColoredName(playerColor) .. " took one clue.", "White") + end + + victoryDisplayApi.update() +end + +-- broadcasts the bless/curse status to the calling player +function showBlessCurseStatus(playerColor) + blessCurseManagerApi.broadcastStatus(playerColor) +end + +-- adds Wendy's menu to the hovered card +function addBlurseSealingMenu(playerColor, hoveredObject) + blessCurseManagerApi.addBlurseSealingMenu(playerColor, hoveredObject) +end + +-- Simple method to check if the given point is in a specified area +---@param point tts__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 + +-- returns the color of the first seated player +function getFirstSeatedPlayer() + for _, color in ipairs(getSeatedPlayers()) do + return color + end +end + +-- returns the colored steam name or color +function getColoredName(playerColor) + local displayName = playerColor + if Player[playerColor].steam_name then + displayName = Player[playerColor].steam_name + end + + -- add bb-code + return "[" .. Color.fromString(playerColor):toHex() .. "]" .. displayName .. "[-]" +end + +function roundToMultiple(num, mult) + return math.floor((num + mult / 2) / mult) * mult +end +end) +__bundle_register("chaosbag/BlessCurseManagerApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local BlessCurseManagerApi = {} + local guidReferenceApi = require("core/GUIDReferenceApi") + + local function getManager() + return guidReferenceApi.getObjectByOwnerAndType("Mythos", "BlessCurseManager") + end + + -- removes all taken tokens and resets the counts + BlessCurseManagerApi.removeTakenTokensAndReset = function() + local BlessCurseManager = getManager() + Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Bless") end, 0.05) + Wait.time(function() BlessCurseManager.call("removeTakenTokens", "Curse") end, 0.10) + Wait.time(function() BlessCurseManager.call("doReset", "White") end, 0.15) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + BlessCurseManagerApi.sealedToken = function(type, guid) + getManager().call("sealedToken", { type = type, guid = guid }) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag + BlessCurseManagerApi.releasedToken = function(type, guid, fromBag) + getManager().call("releasedToken", { type = type, guid = guid, fromBag = fromBag }) + end + + -- updates the internal count (called by cards that seal bless/curse tokens) + ---@param type string Type of chaos token ("Bless" or "Curse") + ---@param guid string GUID of the token + BlessCurseManagerApi.returnedToken = function(type, guid) + getManager().call("returnedToken", { type = type, guid = guid }) + end + + -- broadcasts the current status for bless/curse tokens + ---@param playerColor string Color of the player to show the broadcast to + BlessCurseManagerApi.broadcastStatus = function(playerColor) + getManager().call("broadcastStatus", playerColor) + end + + -- removes all bless / curse tokens from the chaos bag and play + ---@param playerColor string Color of the player to show the broadcast to + BlessCurseManagerApi.removeAll = function(playerColor) + getManager().call("doRemove", playerColor) + end + + -- adds bless / curse sealing to the hovered card + ---@param playerColor string Color of the player to show the broadcast to + ---@param hoveredObject tts__Object Hovered object + BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject) + getManager().call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject }) + end + + -- adds bless / curse to the chaos bag + ---@param type string Type of chaos token ("Bless" or "Curse") + BlessCurseManagerApi.addToken = function(type) + getManager().call("addToken", type) + end + + -- removes bless / curse from the chaos bag + ---@param type string Type of chaos token ("Bless" or "Curse") + BlessCurseManagerApi.removeToken = function(type) + getManager().call("removeToken", type) + end + + BlessCurseManagerApi.getBlessCurseInBag = function() + return getManager().call("getBlessCurseInBag", {}) + end + + return BlessCurseManagerApi +end +end) +__bundle_register("playermat/PlayermatApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local PlayermatApi = {} local guidReferenceApi = require("core/GUIDReferenceApi") local searchLib = require("util/SearchLib") + local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } -- Convenience function to look up a mat's object by color, or get all mats. - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - ---@return table: Single-element if only single playmat is requested + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + ---@return table: Single-element if only single playermat is requested local function getMatForColor(matColor) if matColor == "All" then return guidReferenceApi.getObjectsByType("Playermat") @@ -622,9 +814,9 @@ do end end - -- Returns the color of the closest playmat + -- Returns the color of the closest playermat ---@param startPos table Starting position to get the closest mat from - PlaymatApi.getMatColorByPosition = function(startPos) + PlayermatApi.getMatColorByPosition = function(startPos) local result, smallestDistance for matColor, mat in pairs(getMatForColor("All")) do local distance = Vector.between(startPos, mat.getPosition()):magnitude() @@ -636,17 +828,17 @@ do return result end - -- Returns the color of the player's hand that is seated next to the playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getPlayerColor = function(matColor) + -- Returns the color of the player's hand that is seated next to the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getPlayerColor = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("playerColor") end end - -- Returns the color of the playmat that owns the playercolor's hand - ---@param handColor string Color of the playmat - PlaymatApi.getMatColor = function(handColor) + -- Returns the color of the playermat that owns the playercolor's hand + ---@param handColor string Color of the playermat + PlayermatApi.getMatColor = function(handColor) for matColor, mat in pairs(getMatForColor("All")) do local playerColor = mat.getVar("playerColor") if playerColor == handColor then @@ -655,59 +847,95 @@ do end end - -- Returns if there is the card "Dream-Enhancing Serum" on the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.isDES = function(matColor) + -- Instructs a playermat to check for DES + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.checkForDES = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do - return mat.getVar("isDES") + mat.call("checkForDES") end end - -- Performs a search of the deck area of the requested playmat and returns the result as table - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDeckAreaObjects = function(matColor) + -- Returns if there is the card "Dream-Enhancing Serum" on the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@return boolean: whether DES is present on the playermat + PlayermatApi.hasDES = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("hasDES") + end + end + + -- gets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getSlotData = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getTable("slotData") + end + end + + -- sets the slot data for the playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param newSlotData table New slot data for the playermat + PlayermatApi.loadSlotData = function(matColor, newSlotData) + for _, mat in pairs(getMatForColor(matColor)) do + mat.setTable("slotData", newSlotData) + mat.call("redrawSlotSymbols") + return + end + end + + -- Performs a search of the deck area of the requested playermat and returns the result as table + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDeckAreaObjects = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getDeckAreaObjects") end end -- Flips the top card of the deck (useful after deck manipulation for Norman Withers) - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.flipTopCardFromDeck = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.flipTopCardFromDeck = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("flipTopCardFromDeck") end end - -- Returns the position of the discard pile of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.getDiscardPosition = function(matColor) + -- Returns the position of the discard pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDiscardPosition = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("returnGlobalDiscardPosition") end end + -- Returns the position of the draw pile of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getDrawPosition = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.call("returnGlobalDrawPosition") + end + end + -- Transforms a local position into a global position ---@param localPos table Local position to be transformed - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.transformLocalPosition = function(localPos, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.transformLocalPosition = function(localPos, matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.positionToWorld(localPos) end end - -- Returns the rotation of the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") - PlaymatApi.returnRotation = function(matColor) + -- Returns the rotation of the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnRotation = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getRotation() end end -- Returns a table with spawn data (position and rotation) for a helper object - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param helperName string Name of the helper object - PlaymatApi.getHelperSpawnData = function(matColor, helperName) + PlayermatApi.getHelperSpawnData = function(matColor, helperName) local resultTable = {} local localPositionTable = { ["Hand Helper"] = {0.05, 0, -1.182}, @@ -724,73 +952,99 @@ do end - -- Triggers the Upkeep for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the Upkeep for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param playerColor string Color of the calling player (for messages) - PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor) + PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doUpkeepFromHotkey", playerColor) 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") + -- Handles discarding for the requested playermat for the provided list of objects + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param objList table List of objects to discard - PlaymatApi.discardListOfObjects = function(matColor, objList) + PlayermatApi.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) + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorId = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do return mat.getVar("activeInvestigatorId") end end - -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If + -- Returns the class of the active investigator + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + PlayermatApi.returnInvestigatorClass = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + return mat.getVar("activeInvestigatorClass") + end + end + + -- Returns the position for encounter card drawing + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") + ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right + PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack) + for _, mat in pairs(getMatForColor(matColor)) do + return Vector(mat.call("getEncounterCardDrawPosition", stack)) + end + end + + -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If -- matchTypes is true, the main card slot snap points will only snap assets, while the -- investigator area point will only snap Investigators. If matchTypes is false, snap points will -- be reset to snap all cards. ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("setLimitSnapsByType", matchCardTypes) end end - -- Sets the requested playmat's draw 1 button to visible + -- Sets the requested playermat's draw 1 button to visible ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("showDrawButton", isDrawButtonVisible) end end - -- Shows or hides the clickable clue counter for the requested playmat + -- Shows or hides the clickable clue counter for the requested playermat ---@param showCounter boolean Whether the clickable counter should be present or not - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.clickableClues = function(showCounter, matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.clickableClues = function(showCounter, matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("clickableClues", showCounter) end end - -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.removeClues = function(matColor) + -- Toggles the use of class textures for the requested playermat + ---@param state boolean Whether the class texture should be used or not + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.useClassTexture = function(state, matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("useClassTexture", state) + end + end + + -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.removeClues = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("removeClues") end end - -- Reports the clue count for the requested playmat + -- Reports the clue count for the requested playermat ---@param useClickableCounters boolean Controls which type of counter is getting checked - PlaymatApi.getClueCount = function(useClickableCounters, matColor) + PlayermatApi.getClueCount = function(useClickableCounters, matColor) local count = 0 for _, mat in pairs(getMatForColor(matColor)) do count = count + mat.call("getClueCount", useClickableCounters) @@ -798,44 +1052,41 @@ do return count end - -- updates the specified owned counter - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Updates the specified owned counter + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param type string Counter to target ---@param newValue number Value to set the counter to ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier - PlaymatApi.updateCounter = function(matColor, type, newValue, modifier) + PlayermatApi.updateCounter = function(matColor, type, newValue, modifier) for _, mat in pairs(getMatForColor(matColor)) do mat.call("updateCounter", { type = type, newValue = newValue, modifier = modifier }) end end - -- triggers the draw function for the specified playmat - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Triggers the draw function for the specified playermat + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param number number Amount of cards to draw - PlaymatApi.drawCardsWithReshuffle = function(matColor, number) + PlayermatApi.drawCardsWithReshuffle = function(matColor, number) for _, mat in pairs(getMatForColor(matColor)) do mat.call("drawCardsWithReshuffle", number) end end - -- returns the resource counter amount - ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + -- Returns the resource counter amount + ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support "All") ---@param type string Counter to target - PlaymatApi.getCounterValue = function(matColor, type) + PlayermatApi.getCounterValue = function(matColor, type) for _, mat in pairs(getMatForColor(matColor)) do return mat.call("getCounterValue", type) end end - -- returns a list of mat colors that have an investigator placed - PlaymatApi.getUsedMatColors = function() - local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 } + -- Returns a list of mat colors that have an investigator placed + PlayermatApi.getUsedMatColors = function() local usedColors = {} - for matColor, mat in pairs(getMatForColor("All")) do local searchPos = mat.positionToWorld(localInvestigatorPosition) local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") - if #searchResult > 0 then table.insert(usedColors, matColor) end @@ -843,18 +1094,39 @@ do return usedColors end - -- resets the specified skill tracker to "1, 1, 1, 1" - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.resetSkillTracker = function(matColor) + -- Returns investigator name + ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support "All") + PlayermatApi.getInvestigatorName = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + local searchPos = mat.positionToWorld(localInvestigatorPosition) + local searchResult = searchLib.atPosition(searchPos, "isCardOrDeck") + if #searchResult == 1 then + return searchResult[1].getName() + end + end + return "" + end + + -- Resets the specified skill tracker to "1, 1, 1, 1" + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.resetSkillTracker = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("resetSkillTracker") end end - -- finds all objects on the playmat and associated set aside zone and returns a table - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All + -- Redraws the XML for the slot symbols based on the slotData table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.redrawSlotSymbols = function(matColor) + for _, mat in pairs(getMatForColor(matColor)) do + mat.call("redrawSlotSymbols") + end + end + + -- Finds all objects on the playermat and associated set aside zone and returns a table + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All ---@param filter string Name of the filte function (see util/SearchLib) - PlaymatApi.searchAroundPlaymat = function(matColor, filter) + PlayermatApi.searchAroundPlayermat = function(matColor, filter) local objList = {} for _, mat in pairs(getMatForColor(matColor)) do for _, obj in ipairs(mat.call("searchAroundSelf", filter)) do @@ -865,33 +1137,85 @@ do end -- Discard a non-hidden card from the corresponding player's hand - ---@param matColor string Color of the playmat - White, Orange, Green, Red or All - PlaymatApi.doDiscardOne = function(matColor) + ---@param matColor string Color of the playermat - White, Orange, Green, Red or All + PlayermatApi.doDiscardOne = function(matColor) for _, mat in pairs(getMatForColor(matColor)) do mat.call("doDiscardOne") end end - -- Triggers the metadata sync for all playmats - PlaymatApi.syncAllCustomizableCards = function() + -- Triggers the metadata sync for all playermats + PlayermatApi.syncAllCustomizableCards = function() for _, mat in pairs(getMatForColor("All")) do mat.call("syncAllCustomizableCards") end end - return PlaymatApi + return PlayermatApi +end +end) +__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules) +do + local GUIDReferenceApi = {} + + local function getGuidHandler() + return getObjectFromGUID("123456") + end + + -- Returns the matching object + ---@param owner string Parent object for this search + ---@param type string Type of object to search for + ---@return any: Object reference to the matching object + GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type) + return getGuidHandler().call("getObjectByOwnerAndType", { owner = owner, type = type }) + end + + -- Returns all matching objects as a table with references + ---@param type string Type of object to search for + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByType = function(type) + return getGuidHandler().call("getObjectsByType", type) + end + + -- Returns all matching objects as a table with references + ---@param owner string Parent object for this search + ---@return table: List of object references to matching objects + GUIDReferenceApi.getObjectsByOwner = function(owner) + return getGuidHandler().call("getObjectsByOwner", owner) + end + + -- Sends new information to the reference handler to edit the main index + ---@param owner string Parent of the object + ---@param type string Type of the object + ---@param guid string GUID of the object + GUIDReferenceApi.editIndex = function(owner, type, guid) + return getGuidHandler().call("editIndex", { + owner = owner, + type = type, + guid = guid + }) + end + + -- Returns the owner of an object or the object it's located on + ---@param object tts__GameObject Object for this search + ---@return string: Parent of the object or object it's located on + GUIDReferenceApi.getOwnerOfObject = function(object) + return getGuidHandler().call("getOwnerOfObject", object) + end + + return GUIDReferenceApi end end) __bundle_register("util/SearchLib", function(require, _LOADED, __bundle_register, __bundle_modules) do local SearchLib = {} local filterFunctions = { - isActionToken = function(x) return x.getDescription() == "Action Token" end, isCard = function(x) return x.type == "Card" end, isDeck = function(x) return x.type == "Deck" end, isCardOrDeck = function(x) return x.type == "Card" or x.type == "Deck" end, isClue = function(x) return x.memo == "clueDoom" and x.is_face_down == false end, - isTileOrToken = function(x) return x.type == "Tile" end + isTileOrToken = function(x) return x.type == "Tile" end, + isUniversalToken = function(x) return x.getMemo() == "universalActionAbility" end, } -- performs the actual search and returns a filtered list of object references @@ -915,7 +1239,7 @@ do max_distance = maxDistance or 0 }) - -- filtering the result + -- filter the result for matching objects local objList = {} for _, v in ipairs(searchResult) do if not filter or filterFunc(v.hit_object) then @@ -932,21 +1256,22 @@ do -- searches the area on an object SearchLib.onObject = function(obj, filter) - pos = obj.getPosition() - size = obj.getBounds().size:setAt("y", 1) + local pos = obj.getPosition() + local size = obj.getBounds().size:setAt("y", 1) return returnSearchResult(pos, _, size, filter) end -- searches the specified position (a single point) SearchLib.atPosition = function(pos, filter) - size = { 0.1, 2, 0.1 } + local size = { 0.1, 2, 0.1 } return returnSearchResult(pos, _, size, filter) end -- searches below the specified position (downwards until y = 0) SearchLib.belowPosition = function(pos, filter) - direction = { 0, -1, 0 } - maxDistance = pos.y + local size = { 0.1, 2, 0.1 } + local direction = { 0, -1, 0 } + local maxDistance = pos.y return returnSearchResult(pos, _, size, filter, direction, maxDistance) end