diff --git a/unpacked.ttslua b/unpacked.ttslua
index a30978f69..ef5bee134 100644
--- a/unpacked.ttslua
+++ b/unpacked.ttslua
@@ -41,1505 +41,46 @@ local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (
return require, loaded, register, modules
end)(nil)
-__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 soundCubeApi = require("core/SoundCubeApi")
-local tokenArrangerApi = require("accessories/TokenArrangerApi")
-local tokenChecker = require("core/token/TokenChecker")
-local tokenManager = require("core/token/TokenManager")
-
----------------------------------------------------------
--- general setup
----------------------------------------------------------
-
-ENCOUNTER_DECK_POS = { -3.93, 1, 5.76 }
-ENCOUNTER_DECK_DISCARD_POSITION = { -3.85, 1, 10.38 }
-
--- GUIDs that will not be interactable (e.g. parts of the table)
-local NOT_INTERACTABLE = {
- "6161b4", -- Decoration-Map
- "721ba2", -- PlayArea
- "9f334f", -- MythosArea
- "463022", -- Panel behind tentacle stand
- "f182ee", -- InvestigatorCount
- "7bff34", -- Tentacle stand
- "8646eb", -- horizontal border left
- "75937e", -- horizontal border right
- "612072", -- vertical border left
- "975c39", -- vertical border right
-}
-
--- global variable for access
-chaosTokens = {}
-local chaosTokensLastMat = nil
-
-local bagSearchers = {}
-local MAT_COLORS = { "White", "Orange", "Green", "Red" }
-local hideTitleSplashWaitFunctionId = nil
-
--- online functionality related variables
-local MOD_VERSION = "3.4.0"
-local SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'
-local library, requestObj, modMeta
-local acknowledgedUpgradeVersions = {}
-local contentToShow = "campaigns"
-local currentListItem = 1
-local xmlVisibility = {
- downloadWindow = false,
- optionPanel = false,
- playareaGallery = false,
- updateNotification = false
-}
-local tabIdTable = {
- tab1 = "campaigns",
- tab2 = "scenarios",
- tab3 = "fanmadeCampaigns",
- tab4 = "fanmadeScenarios",
- tab5 = "fanmadePlayerCards"
-}
-
--- optionPanel data
-optionPanel = {}
-local LANGUAGES = {
- { code = "zh_CN", name = "简体中文" },
- { code = "zh_TW", name = "繁體中文" },
- { code = "de", name = "Deutsch" },
- { code = "en", name = "English" },
- { code = "es", name = "Español" },
- { code = "fr", name = "Français" },
- { code = "it", name = "Italiano" }
-}
-local RESOURCE_OPTIONS = {
- "enabled",
- "custom",
- "disabled"
-}
-
----------------------------------------------------------
--- data for tokens
----------------------------------------------------------
-
-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}}
-}
-
-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/'}
-}
-
----------------------------------------------------------
--- data for chaos token stat tracker
----------------------------------------------------------
-
-local tokenDrawingStats = {
- ["Overall"] = {},
- ["8b081b"] = {},
- ["bd0ff4"] = {},
- ["383d8b"] = {},
- ["0840d5"] = {}
-}
-
----------------------------------------------------------
--- general code
----------------------------------------------------------
-
--- saving state of optionPanel to restore later
-function onSave()
- return JSON.encode({
- optionPanel = optionPanel,
- acknowledgedUpgradeVersions = acknowledgedUpgradeVersions
- })
-end
-
-function onLoad(savedData)
- if savedData then
- loadedData = JSON.decode(savedData)
- optionPanel = loadedData.optionPanel
- acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions
- updateOptionPanelState()
- else
- print("Saved state could not be found!")
- end
-
- for _, guid in ipairs(NOT_INTERACTABLE) do
- local obj = getObjectFromGUID(guid)
- if obj ~= nil then obj.interactable = false end
- end
-
- resetChaosTokenStatTracker()
- getModVersion()
- math.randomseed(os.time())
-
- -- initialization of loadable objects library (delay to let Navigation Overlay build)
- Wait.time(function()
- WebRequest.get(SOURCE_REPO .. '/library.json', libraryDownloadCallback)
- 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
--- chaos bag during search operations to avoid this.
-function onObjectSearchStart(object, playerColor)
- chaosbag = findChaosBag()
- if object == chaosbag then
- bagSearchers[playerColor] = true
- 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
--- chaos bag during search operations to avoid this.
-function onObjectSearchEnd(object, playerColor)
- chaosbag = findChaosBag()
- if object == chaosbag then
- bagSearchers[playerColor] = nil
- end
-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)
- 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())
- local trash = guidReferenceApi.getObjectByOwnerAndType(matcolor, "Trash")
- trash.putObject(enteringObj)
- end
-end
-
----------------------------------------------------------
--- chaos token drawing
----------------------------------------------------------
-
--- checks scripting zone for chaos bag (also called by a lot of objects!)
-function findChaosBag()
- local chaosbag_zone = getObjectFromGUID("83ef06")
-
- -- error handling: scripting zone not found
- if chaosbag_zone == nil then
- printToAll("Zone for chaos bag detection couldn't be found.", "Red")
- return
- end
-
- for _, item in ipairs(chaosbag_zone.getObjects()) do
- if item.getDescription() == "Chaos Bag" then
- return item
- end
- end
-
- -- error handling: chaos bag not found
- printToAll("Chaos bag couldn't be found.", "Red")
-end
-
-function returnChaosTokens()
- for _, token in pairs(chaosTokens) do
- if token ~= nil then chaosbag.putObject(token) end
- end
- chaosTokens = {}
-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
- if searching then
- broadcastToAll("Someone is searching the chaos bag, can't touch the tokens.", "Red")
- return false
- end
- end
- return true
-end
-
--- called by playermats (by the "Draw chaos token" button)
-function drawChaosToken(params)
- if not canTouchChaosTokens() then return end
-
- local mat = params[1]
- local tokenOffset = params[2]
- local isRightClick = params[3]
- chaosbag = findChaosBag()
-
- -- return token(s) on other playmat first
- if chaosTokensLastMat ~= nil and chaosTokensLastMat ~= mat and #chaosTokens ~= 0 then
- returnChaosTokens()
- chaosTokensLastMat = nil
- return
- end
-
- chaosTokensLastMat = mat
-
- -- if we have left clicked and have no tokens OR if we have right clicked
- if isRightClick or #chaosTokens == 0 then
- if #chaosbag.getObjects() == 0 then return end
- chaosbag.shuffle()
-
- -- add the token to the list, compute new position based on list length
- tokenOffset[1] = tokenOffset[1] + (0.17 * #chaosTokens)
- local token = chaosbag.takeObject({
- index = 0,
- position = mat.positionToWorld(tokenOffset),
- rotation = mat.getRotation()
- })
-
- -- 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, mat.getGUID())
- chaosTokens[#chaosTokens + 1] = token
- return
- else
- returnChaosTokens()
- end
-end
-
----------------------------------------------------------
--- token spawning
----------------------------------------------------------
-
--- DEPRECATED. Use TokenManager instead.
--- Spawns a single token.
----@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
-
----------------------------------------------------------
--- chaos token stat tracker
----------------------------------------------------------
-
-function trackChaosToken(tokenName, matGUID)
- tokenDrawingStats["Overall"][tokenName] = (tokenDrawingStats["Overall"][tokenName] or 0) + 1
- tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + 1
-end
-
--- Left-click: print stats, Right-click: reset stats
-function handleStatTrackerClick(_, _, isRightClick)
- if isRightClick then
- resetChaosTokenStatTracker()
- else
- local squidKing = "Nobody"
- local maxSquid = 0
- local foundAnyStats = false
-
- for key, personalStats in pairs(tokenDrawingStats) do
- local playerColor, playerName
-
- if key == "Overall" then
- playerColor = "White"
- playerName = "Overall"
- else
- -- get mat color
- local matColor = playmatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition())
- playerColor = playmatApi.getPlayerColor(matColor)
- playerName = Player[playerColor].steam_name or playerColor
-
- local playerSquidCount = personalStats["Auto-fail"]
- if playerSquidCount > maxSquid then
- squidKing = playerName
- maxSquid = playerSquidCount
- end
- end
-
- -- get the total count of drawn tokens for the player
- local totalCount = 0
- for tokenName, value in pairs(personalStats) do
- totalCount = totalCount + value
- end
-
- -- only print the personal stats if any tokens were drawn
- if totalCount > 0 then
- foundAnyStats = true
- printToAll("------------------------------")
- printToAll(playerName .. " Stats", playerColor)
-
- for tokenName, value in pairs(personalStats) do
- if value ~= 0 then
- printToAll(tokenName .. ': ' .. tostring(value))
- end
- end
- printToAll('Total: ' .. tostring(totalCount))
- end
- end
-
- -- detect if any player drew tokens
- if foundAnyStats then
- printToAll("------------------------------")
- printToAll(squidKing .. " is an auto-fail magnet.", { 255, 0, 0 })
- else
- printToAll("No tokens have been drawn yet.", "Yellow")
- end
- end
-end
-
--- resets the count for each token to 0
-function resetChaosTokenStatTracker()
- for key, _ in pairs(tokenDrawingStats) do
- tokenDrawingStats[key] = {}
- for _, token in pairs(ID_URL_MAP) do
- tokenDrawingStats[key][token.name] = 0
- end
- end
-end
-
----------------------------------------------------------
--- Difficulty selector script
----------------------------------------------------------
-
--- called for button creation on the difficulty selectors
----@param object object Usually "self"
----@param key string Name of the scenario
-function createSetupButtons(args)
- local data = getDataValue('modeData', args.key)
- if data ~= nil then
- local buttonParameters = {}
- buttonParameters.function_owner = args.object
- buttonParameters.position = { 0, 0.1, -0.15 }
- buttonParameters.scale = { 0.47, 1, 0.47 }
- buttonParameters.height = 200
- buttonParameters.width = 1150
- buttonParameters.color = { 0.87, 0.8, 0.7 }
-
- if data.easy ~= nil then
- buttonParameters.label = "Easy"
- buttonParameters.click_function = "easyClick"
- args.object.createButton(buttonParameters)
- buttonParameters.position[3] = buttonParameters.position[3] + 0.20
- end
-
- if data.normal ~= nil then
- buttonParameters.label = "Standard"
- buttonParameters.click_function = "normalClick"
- args.object.createButton(buttonParameters)
- buttonParameters.position[3] = buttonParameters.position[3] + 0.20
- end
-
- if data.hard ~= nil then
- buttonParameters.label = "Hard"
- buttonParameters.click_function = "hardClick"
- args.object.createButton(buttonParameters)
- buttonParameters.position[3] = buttonParameters.position[3] + 0.20
- end
-
- if data.expert ~= nil then
- buttonParameters.label = "Expert"
- buttonParameters.click_function = "expertClick"
- args.object.createButton(buttonParameters)
- buttonParameters.position[3] = buttonParameters.position[3] + 0.20
- end
-
- if data.standalone ~= nil then
- buttonParameters.label = "Standalone"
- buttonParameters.click_function = "standaloneClick"
- args.object.createButton(buttonParameters)
- end
- end
-end
-
--- called for adding chaos tokens
----@param object object Usually "self"
----@param key string Name of the scenario
----@param mode string difficulty (e.g. "hard" or "expert")
-function fillContainer(args)
- local data = getDataValue('modeData', args.key)
- if data == nil then return end
-
- local value = data[args.mode]
- if value == nil or value.token == nil then return end
-
- local tokenList = {}
-
- for _, tokenId in ipairs(value.token) do
- table.insert(tokenList, tokenId)
- end
-
- if value.append ~= nil then
- for _, tokenId in ipairs(value.append) do
- table.insert(tokenList, tokenId)
- end
- end
-
- -- randomly choose tokens for specific Carcosa scenarios in standalone
- if value.random then
- local n = #value.random
- if n > 0 then
- for _, tokenId in ipairs(value.random[math.random(1, n)]) do
- table.insert(tokenList, tokenId)
- end
- end
- end
-
- setChaosBagState(tokenList)
-
- if value.message then
- broadcastToAll(value.message)
- end
-
- if value.warning then
- broadcastToAll(value.warning, { 1, 0.5, 0.5 })
- end
-end
-
-function getDataValue(storage, key)
- local DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper")
- local data = DATA_HELPER.getTable(storage)
- if data ~= nil then
- local value = data[key]
- if value ~= nil then
- local res = {}
- for m, v in pairs(value) do
- res[m] = v
- if res[m].parent ~= nil then
- local parentData = getDataValue(storage, res[m].parent)
- if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then
- res[m].token = parentData[m].token
- end
- res[m].parent = nil
- end
- end
- return res
- end
- end
-end
-
-function createChaosTokenNameLookupTable()
- local namesToIds = {}
- for k, v in pairs(ID_URL_MAP) do
- namesToIds[v.name] = k
- end
- return namesToIds
-end
-
--- returns a Table List of chaos token ids in the current chaos bag
----@api chaosbag/ChaosBagApi
-function getChaosBagState()
- local tokens = {}
- local invertedTable = createChaosTokenNameLookupTable()
- local chaosbag = findChaosBag()
-
- for _, v in ipairs(chaosbag.getObjects()) do
- local id = invertedTable[v.name]
- if id then
- table.insert(tokens, id)
- else
- printToAll(v.name .. " token not recognized. Will not be recorded.", "Yellow")
- end
- end
-
- return tokens
-end
-
--- respawns the chaos bag with a new state of tokens
----@param tokenList Table List of chaos token ids
----@api chaosbag/ChaosBagApi
-function setChaosBagState(tokenList)
- if not canTouchChaosTokens() then return end
-
- local chaosbag = findChaosBag()
- local chaosbagData = chaosbag.getData()
- local reserveData = getObjectFromGUID("106418").getData()
- local tokenCache = {}
- local containedObjects = {}
-
- -- create a temporary copy of the data for each chaos token
- for _, objData in ipairs(reserveData.ContainedObjects) do
- tokenCache[objData.Nickname] = objData
- end
-
- -- iterate over tokenlist and insert specified tokens into new table
- for _, tokenId in ipairs(tokenList) do
- local tokenName = ID_URL_MAP[tokenId].name
- table.insert(containedObjects, tokenCache[tokenName])
- end
-
- -- overwrite chaos bag content and respawn it
- chaosbagData.ContainedObjects = containedObjects
- chaosbag.destruct()
- spawnObjectData({ data = chaosbagData })
-
- -- remove tokens that are still in play
- for _, token in pairs(chaosTokens) do
- if token ~= nil then token.destruct() end
- end
- chaosTokens = {}
- chaosTokensLastMat = nil
-
- -- reset bless / curse manager
- blessCurseManagerApi.removeTakenTokensAndReset()
-
- printToAll("Chaos bag set to chosen difficulty.", "Green")
-end
-
--- spawns the specified chaos token and puts it into the chaos bag
----@param id String ID of the chaos token
-function spawnChaosToken(id)
- if not canTouchChaosTokens() then return end
-
- id = id:lower()
- local chaosbag = findChaosBag()
- local url = ID_URL_MAP[id].url or ""
-
- if url ~= "" then
- return spawnObject({
- type = 'Custom_Tile',
- position = { 0.49, 3, 0 },
- scale = { 0.81, 1.0, 0.81 },
- rotation = { 0, 270, 0 },
- callback_function = function(obj)
- obj.setName(ID_URL_MAP[id].name)
- chaosbag.putObject(obj)
- tokenArrangerApi.layout()
- end
- }).setCustomObject({
- type = 2,
- image = url,
- thickness = 0.1
- })
- end
-end
-
--- removes the specified chaos token from the chaos bag
----@param id String ID of the chaos token
-function removeChaosToken(id)
- if not canTouchChaosTokens() then return end
-
- local tokens = {}
- local chaosbag = findChaosBag()
- local name = ID_URL_MAP[id].name
-
- for _, v in ipairs(chaosbag.getObjects()) do
- if v.name == name then table.insert(tokens, v.guid) end
- end
-
- -- error handling: no matching token found
- if #tokens == 0 then
- printToAll("No " .. name .. " tokens in the chaos bag.", "Yellow")
- return
- end
-
- chaosbag.takeObject({
- guid = tokens[1],
- smooth = false,
- callback_function = function(obj)
- obj.destruct()
- tokenArrangerApi.layout()
- end
- })
- 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
-end
-
----------------------------------------------------------
--- Content Importing and XML functions
----------------------------------------------------------
-
--- forwards the requested content type to the update function and sets highlight to clicked tab
----@param tabId String Id of the clicked tab
-function onClick_tab(_, _, tabId)
- for listId, listContent in pairs(tabIdTable) do
- if listId == tabId then
- UI.setClass(listId, 'downloadTab activeTab')
- contentToShow = listContent
- else
- UI.setClass(listId, 'downloadTab')
- end
- end
- currentListItem = 1
- updateDownloadItemList()
-end
-
--- click function for the items in the download window
--- updates backgroundcolor for row panel and fontcolor for list item
-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
- currentListItem = nil
- for str in string.gmatch(identificationKey, "([^_]+)") do
- if not contentToShow then
- -- grab the first part to know the content type
- contentToShow = str
- else
- -- get the index
- currentListItem = tonumber(str)
- break
- end
- end
- end
-
- UI.setAttribute("panel" .. currentListItem, "color", "grey")
- UI.setAttribute(contentToShow .. "_" .. currentListItem, "color", "black")
- updatePreviewWindow()
-end
-
--- click function for the download button in the preview window
-function onClick_download(player)
- local params = library[contentToShow][currentListItem]
- params.player = player
- placeholder_download(params)
-end
-
--- the download button on the placeholder objects calls this to directly initiate a download
----@param param Table contains url and guid of replacement object
-function placeholder_download(params)
- local url = SOURCE_REPO .. '/' .. params.url
- requestObj = WebRequest.get(url, function (request) contentDownloadCallback(request, params) end)
- startLuaCoroutine(Global, 'downloadCoroutine')
-end
-
-function downloadCoroutine()
- -- show progress bar
- UI.setAttribute('download_progress', 'active', true)
-
- -- update progress bar
- while requestObj do
- UI.setAttribute('download_progress', 'percentage', requestObj.download_progress * 100)
- coroutine.yield(0)
- end
- UI.setAttribute('download_progress', 'percentage', 100)
-
- -- wait 30 frames
- for i = 1, 30 do
- coroutine.yield(0)
- end
-
- -- hide progress bar
- UI.setAttribute('download_progress', 'active', false)
-
- -- hide download window
- if xmlVisibility.downloadWindow then
- xmlVisibility.downloadWindow = false
- UI.hide('downloadWindow')
- end
- return 1
-end
-
--- spawns a bag that contains every object from the library
-function onClick_downloadAll()
- broadcastToAll("Download initiated - this will take a few minutes!")
-
- -- hide download window
- if xmlVisibility.downloadWindow then
- xmlVisibility.downloadWindow = false
- UI.hide('downloadWindow')
- end
-
- startLuaCoroutine(Global, "coroutineDownloadAll")
-end
-
-function coroutineDownloadAll()
- local JSON = [[
- {
- "Name": "Bag",
- "Transform": {
- "posX": -39.5,
- "posY": 2,
- "posZ": -87,
- "rotX": 0,
- "rotY": 270,
- "rotZ": 0,
- "scaleX": 1.0,
- "scaleY": 1.0,
- "scaleZ": 1.0
- },
- "Nickname": "All Downloadable Content",
- "Bag": {
- "Order": 0
- },
- "ContainedObjects": [
- ]]
-
- local contained = ""
- local downloadedItems = 0
- local skippedItems = 0
-
- -- loop through the library to add content
- for contentType, objectList in pairs(library) do
- broadcastToAll("Downloading " .. contentType .. "...")
- for _, params in ipairs(objectList) do
- local request = WebRequest.get(SOURCE_REPO .. '/' .. params.url)
- local start = os.time()
- while true do
- if request.is_done then
- contained = contained .. request.text .. ","
- downloadedItems = downloadedItems + 1
- break
- -- time-out if item can't be loaded in 5s
- elseif request.is_error or (os.time() - start) > 5 then
- skippedItems = skippedItems + 1
- break
- end
- coroutine.yield(0)
- end
- end
- end
-
- JSON = JSON .. contained .. "]}"
- spawnObjectJSON({json = JSON})
-
- broadcastToAll(downloadedItems .. " objects downloaded.", "Green")
- broadcastToAll(skippedItems .. " objects had a time-out / error.", "Orange")
- return 1
-end
-
--- spawns a placeholder box for the selected object
-function onClick_spawnPlaceholder()
- -- get object references
- local item = library[contentToShow][currentListItem]
- local dummy = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlaceholderBoxDummy")
-
- -- error handling
- if not item.boxsize or item.boxsize == "" or not item.boxart or item.boxart == "" then
- print("Error loading object.")
- return
- end
-
- -- get data for placeholder
- local spawnPos = {-39.5, 2, -87}
-
- local meshTable = {
- big = "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj",
- small = "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj",
- wide = "http://pastebin.com/raw.php?i=uWAmuNZ2"
- }
-
- local scaleTable = {
- 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},
- scale = scaleTable[item.boxsize],
- })
-
- placeholder.setCustomObject({
- mesh = meshTable[item.boxsize],
- diffuse = item.boxart,
- material = 3
- })
-
- placeholder.setColorTint({1, 1, 1, 71/255})
- placeholder.setName(item.name)
- placeholder.setDescription("by " .. (item.author or "Unknown"))
- placeholder.setGMNotes(item.url)
- placeholder.setLuaScript(dummy.getLuaScript())
- Player.getPlayers()[1].pingTable(spawnPos)
-
- -- hide download window
- if xmlVisibility.downloadWindow then
- xmlVisibility.downloadWindow = false
- UI.hide('downloadWindow')
- end
-end
-
--- toggles the visibility of the respective UI
----@param player LuaPlayer Player that triggered this
----@param title String Name of the UI to toggle
-function onClick_toggleUi(player, title)
- if title == "Navigation Overlay" then
- navigationOverlayApi.cycleVisibility(player.color)
- return
- -- hide the playareaGallery if visible
- elseif title == "downloadWindow" and xmlVisibility.playareaGallery then
- onClick_toggleUi(_, "playareaGallery")
- -- hide the downloadWindow if visible
- elseif title == "playareaGallery" and xmlVisibility.downloadWindow then
- onClick_toggleUi(_, "downloadWindow")
- end
-
- if xmlVisibility[title] then
- -- small delay to allow button click sounds to play
- Wait.time(function() UI.hide(title) end, 0.1)
- else
- UI.show(title)
- end
- xmlVisibility[title] = not xmlVisibility[title]
-end
-
--- forwards the call to the onClick function
-function togglePlayareaGallery()
- onClick_toggleUi(_, "playareaGallery")
-end
-
--- updates the preview window
-function updatePreviewWindow()
- local item = library[contentToShow][currentListItem]
- 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
- item.boxsize = "big"
- item.boxart = "http://cloud-3.steamusercontent.com/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/"
- end
-
- UI.setValue("previewTitle", item.name)
- UI.setValue("previewAuthor", "by " .. (item.author or "- Author not found -"))
- UI.setValue("previewDescription", item.description or "- Description not found -")
-
- -- update mask according to size (hardcoded values to align image in mask)
- local maskData = {}
- if item.boxsize == "big" then
- maskData = {
- image = "box-cover-mask-big",
- width = "870",
- height = "435",
- offsetXY = "154 60"
- }
- elseif item.boxsize == "small" then
- maskData = {
- image = "box-cover-mask-small",
- width = "792",
- height = "594",
- offsetXY = "135 13"
- }
- elseif item.boxsize == "wide" then
- maskData = {
- image = "box-cover-mask-wide",
- width = "756",
- height = "630",
- offsetXY = "-190 -70"
- }
- end
-
- -- 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)
-end
-
--- formats the json response from the webrequest into a key-value lua table
--- strips the prefix from the community content items
-function formatLibrary(json_response)
- library = {}
- library["campaigns"] = json_response.campaigns
- library["scenarios"] = json_response.scenarios
- library["extras"] = json_response.extras
- library["fanmadeCampaigns"] = {}
- library["fanmadeScenarios"] = {}
- library["fanmadePlayerCards"] = {}
-
- for _, item in ipairs(json_response.community) do
- local identifier = nil
- for str in string.gmatch(item.name, "([^:]+)") do
- if not identifier then
- -- grab the first part to know the content type
- identifier = str
- else
- -- update the name without the content type
- item.name = str
- break
- end
- end
-
- if identifier == "Fan Investigators" then
- table.insert(library["fanmadePlayerCards"], item)
- elseif identifier == "Fan Campaign" then
- table.insert(library["fanmadeCampaigns"], item)
- elseif identifier == "Fan Scenario" then
- table.insert(library["fanmadeScenarios"], item)
- end
- end
-end
-
--- updates the window content to the requested content
-function updateDownloadItemList()
- if not library then return end
-
- -- addition of list items according to library file
- local globalXml = UI.getXmlTable()
- local contentList = getXmlTableElementById(globalXml, 'contentList')
-
- contentList.children = {}
- for i, v in ipairs(library[contentToShow]) do
- table.insert(contentList.children,
- {
- tag = "Panel",
- attributes = { id = "panel" .. i },
- children = {
- tag = 'Text',
- value = v.name,
- attributes = {
- id = contentToShow .. "_" .. i,
- onClick = 'onClick_select',
- alignment = 'MiddleLeft'
- }
- }
- })
- end
-
- contentList.attributes.height = #contentList.children * 27
- UI.setXmlTable(globalXml)
-
- -- select the first item
- Wait.time(onClick_select, 0.2)
-end
-
--- called after the webrequest of downloading an item
--- deletes the placeholder and spawns the downloaded item
-function contentDownloadCallback(request, params)
- requestObj = nil
-
- -- error handling
- if request.is_error or request.response_code ~= 200 then
- print('Error: ' .. request.error)
- return
- end
-
- -- initiate content spawning
- local spawnTable = { json = request.text }
- if params.replace then
- local replacedObject = getObjectFromGUID(params.replace)
- if replacedObject then
- spawnTable.position = replacedObject.getPosition()
- spawnTable.rotation = replacedObject.getRotation()
- spawnTable.scale = replacedObject.getScale()
- destroyObject(replacedObject)
- end
- end
-
- -- if position is undefined, get empty position
- if not spawnTable.position then
- 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")
- return
- end
- end
-
- -- if spawned from menu, move the camera and/or ping the table
- if params.name then
- spawnTable["callback_function"] = function(obj)
- Wait.time(function()
- -- move camera
- if params.player then
- params.player.lookAt({
- position = obj.getPosition(),
- pitch = 65,
- yaw = 90,
- distance = 65
- })
- end
-
- -- ping object
- local pingPlayer = params.player or Player.getPlayers()[1]
- pingPlayer.pingTable(obj.getPosition())
- end, 0.1)
- end
- end
-
- if pcall(function() spawnObjectJSON(spawnTable) end) then
- print('Object loaded.')
- else
- print('Error loading object.')
- end
-end
-
--- gets the first empty position to spawn a custom content object safely
-function getValidSpawnPosition()
- local potentialSpawnPositionX = { 65, 50, 35 }
- local potentialSpawnPositionY = 1.5
- local potentialSpawnPositionZ = { 35, 21, 7, -7, -21, -35 }
-
- for i, posX in ipairs(potentialSpawnPositionX) do
- for j, posZ in ipairs(potentialSpawnPositionZ) do
- local pos = {
- x = posX,
- y = potentialSpawnPositionY,
- z = posZ,
- }
- if checkPositionForContentSpawn(pos) then
- return pos
- end
- end
- end
- return nil
-end
-
--- checks whether something is in the specified position
--- returns true if empty
-function checkPositionForContentSpawn(checkPos)
- local search = Physics.cast({
- direction = { 0, 1, 0 },
- max_distance = 0.1,
- type = 3,
- size = { 0.1, 0.1, 0.1 },
- origin = checkPos
- })
- -- first hit is the table surface, additional hits means something is there
- return #search == 1
-end
-
--- downloading of the library file
-function libraryDownloadCallback(request)
- if request.is_error or request.response_code ~= 200 then
- print('error: ' .. request.error)
- return
- end
-
- local json_response = nil
- if pcall(function () json_response = JSON.decode(request.text) end) then
- formatLibrary(json_response)
- updateDownloadItemList()
- else
- print('error parsing downloaded library')
- end
-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
-
----------------------------------------------------------
--- 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)
-end
-
--- called by the language selection dropdown
-function languageSelected(_, selectedIndex, id)
- optionPanel[id] = LANGUAGES[tonumber(selectedIndex) + 1].code
-end
-
--- returns the ID (position in the table) for a provided language code
-function returnLanguageId(code)
- for index, tbl in ipairs(LANGUAGES) do
- if tbl.code == code then
- return index
- end
- end
-end
-
--- called by the resource counter selection dropdown
-function resourceCounterSelected(_, selectedIndex, id)
- optionPanel[id] = RESOURCE_OPTIONS[tonumber(selectedIndex) + 1]
-end
-
--- returns the ID for the provided option name
-function returnResourceCounterId(name)
- for index, optionName in ipairs(RESOURCE_OPTIONS) do
- if optionName == name then
- return index
- end
- end
-end
-
--- sets the option panel to the correct state (corresponding to 'optionPanel')
-function updateOptionPanelState()
- for id, optionValue in pairs(optionPanel) do
- if id == "cardLanguage" and type(optionValue) == "string" then
- local dropdownId = returnLanguageId(optionValue) - 1
- UI.setAttribute(id, "value", dropdownId)
- elseif id == "useResourceCounters" and type(optionValue) == "string" then
- local dropdownId = returnResourceCounterId(optionValue) - 1
- UI.setAttribute(id, "value", dropdownId)
- 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)
- else
- UI.setAttribute(id, "isOn", "False")
- end
- end
-end
-
--- handles the applying of option selections and calls the respective functions based
----@param id String ID of the option that was selected or deselected
----@param state Boolean State of the option (true = enabled)
-function applyOptionPanelChange(id, state)
- -- option: Snap tags
- if id == "useSnapTags" then
- playmatApi.setLimitSnapsByType(state, "All")
- optionPanel[id] = state
-
- -- option: Draw 1 button
- elseif id == "showDrawButton" then
- playmatApi.showDrawButton(state, "All")
- optionPanel[id] = state
-
- -- option: Clickable clue counters
- elseif id == "useClueClickers" then
- playmatApi.clickableClues(state, "All")
- optionPanel[id] = state
-
- -- update master clue counter
- local counter = guidReferenceApi.getObjectByOwnerAndType("Mythos", "MasterClueCounter")
- counter.setVar("useClickableCounters", state)
-
- -- 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: Show clean up helper
- elseif id == "showCleanUpHelper" then
- optionPanel[id] = spawnOrRemoveHelper(state, "Clean Up Helper", {-66, 1.6, 46})
-
- -- option: Show hand helper for each player
- elseif id == "showHandHelper" then
- for i, color in ipairs(MAT_COLORS) do
- local pos = playmatApi.transformLocalPosition({0.05, 0, -1.182}, color)
- local rot = playmatApi.returnRotation(color)
- optionPanel[id][i] = spawnOrRemoveHelper(state, "Hand Helper", pos, rot)
- end
-
- -- option: Show search assistant for each player
- elseif id == "showSearchAssistant" then
- for i, color in ipairs(MAT_COLORS) do
- local pos = playmatApi.transformLocalPosition({-0.3, 0, -1.182}, color)
- local rot = playmatApi.returnRotation(color)
- optionPanel[id][i] = spawnOrRemoveHelper(state, "Search Assistant", pos, rot)
- end
-
- -- option: Show attachment helper
- elseif id == "showAttachmentHelper" then
- optionPanel[id] = spawnOrRemoveHelper(state, "Attachment Helper", {-62, 1.4, 0})
-
- -- option: Show CYOA campaign guides
- elseif id == "showCYOA" then
- optionPanel[id] = spawnOrRemoveHelper(state, "CYOA Campaign Guides", {39, 1.3, -20})
-
- -- option: Show displacement tool
- elseif id == "showDisplacementTool" then
- optionPanel[id] = spawnOrRemoveHelper(state, "Displacement Tool", {-57, 1.6, 46})
- end
-end
-
--- handler for spawn / remove functions of helper objects
----@param state Boolean Contains the state of the option: true = spawn it, false = remove it
----@param name String Name of the helper object
----@param position Vector Position of the object (where it will spawn)
----@param rotation Vector Rotation of the object for spawning (default: {0, 270, 0})
----@return. GUID of the spawnedObj (or nil if object was removed)
-function spawnOrRemoveHelper(state, name, position, rotation)
- if (type(state) == "table" and #state == 0) then
- return removeHelperObject(name)
- elseif state then
- Player.getPlayers()[1].pingTable(position)
- return spawnHelperObject(name, position, rotation).getGUID()
- else
- return removeHelperObject(name)
- end
-end
-
--- copies the specified tool (by name) from the option panel source bag
----@param name String Name of the object that should be copied
----@param position Table Desired position of the object
----@param rotation Table Desired rotation of the object (defaults to object's rotation)
-function spawnHelperObject(name, position, rotation)
- local sourceBag = guidReferenceApi.getObjectByOwnerAndType("Mythos","OptionPanelSource")
-
- -- error handling for missing sourceBag
- if not sourceBag then
- broadcastToAll("Option panel source bag could not be found!", "Red")
- return
- end
-
- local spawnTable = { position = position }
-
- -- only overrride rotation if there is one provided (object's rotation used instead)
- if rotation then
- 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
- return spawnObjectData(spawnTable)
- end
- end
-end
-
--- removes the specified tool (by name)
----@param name String Object that should be removed
-function removeHelperObject(name)
- -- links objects name to the respective option name (to grab the GUID for removal)
- local referenceTable = {
- ["Clean Up Helper"] = "showCleanUpHelper",
- ["Hand Helper"] = "showHandHelper",
- ["Search Assistant"] = "showSearchAssistant",
- ["Displacement Tool"] = "showDisplacementTool",
- ["Attachment Helper"] = "showAttachmentHelper",
- ["CYOA Campaign Guides"] = "showCYOA"
- }
-
- local data = optionPanel[referenceTable[name]]
-
- -- if there is a GUID stored, remove that object
- if type(data) == "string" then
- local obj = getObjectFromGUID(data)
- if obj then obj.destruct() end
-
- -- if it is a table (e.g. for the "Hand Helper", remove all of them)
- elseif type(data) == "table" then
- for _, guid in pairs(data) do
- local obj = getObjectFromGUID(guid)
- if obj then obj.destruct() end
- end
- end
-end
-
--- loads saved options
-function loadSettings(newOptions)
- optionPanel = newOptions
- updateOptionPanelState()
- for id, state in pairs(optionPanel) do
- applyOptionPanelChange(id, state)
- end
-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",
- playAreaSnapTags = true,
- showAttachmentHelper = false,
- showCleanUpHelper = false,
- showCYOA = false,
- showDisplacementTool = false,
- showDrawButton = false,
- showHandHelper = {},
- showSearchAssistant = {},
- showTitleSplash = true,
- useClueClickers = false,
- useResourceCounters = "disabled",
- useSnapTags = true
- }
-
- -- update UI
- updateOptionPanelState()
-end
-
--- splash scenario title on setup
-function titleSplash(scenarioName)
- if optionPanel['showTitleSplash'] then
- -- if there's any ongoing title being displayed, hide it and cancel the waiting function
- if hideTitleSplashWaitFunctionId then
- Wait.stop(hideTitleSplashWaitFunctionId)
- hideTitleSplashWaitFunctionId = nil
- UI.setAttribute('title_splash', 'active', false)
- end
-
- -- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)
- -- wait timer to hide the scenario name
- UI.setValue('title_splash_text', scenarioName)
- UI.show('title_splash')
- hideTitleSplashWaitFunctionId = Wait.time(function()
- UI.hide('title_splash')
- hideTitleSplashWaitFunctionId = nil
- end, 4)
-
- soundCubeApi.playSoundByName("Deep Bell")
- end
-end
-
----------------------------------------------------------
--- Update notification related functionality
----------------------------------------------------------
-
--- grabs the latest mod version and release notes from GitHub (called onLoad())
-function getModVersion()
- WebRequest.get(SOURCE_REPO .. '/modversion.json', compareVersion)
-end
-
--- compares the modversion with GitHub and possibly shows the update notification
-function compareVersion(request)
- if request.is_error then
- log(request.error)
- return
- end
-
- -- global variable to make it accessible for other functions
- modMeta = JSON.decode(request.text)
-
- -- stop here if on latest or newer version
- if convertVersionToNumber(MOD_VERSION) >= convertVersionToNumber(modMeta["latestVersion"]) then return end
-
- -- stop here if "don't show again" was clicked for this version before
- if acknowledgedUpgradeVersions[modMeta["latestVersion"]] then return end
-
- updateNotificationLoading()
-
- -- delay to avoid lagging during onLoad()
- Wait.time(function() UI.show("FinnIcon") end, 1)
-end
-
--- converts a version number to a string
----@param version String Version number, separated by dots (e.g. 3.3.1)
-function convertVersionToNumber(version)
- local major, minor, patch = string.match(version, "(%d+)%.(%d+)%.(%d+)")
- return major * 100 + minor * 10 + patch
-end
-
--- updates the XML update notification based on the mod metadata
-function updateNotificationLoading()
- -- grab data
- local highlights = modMeta["releaseHighlights"]
-
- -- concatenate the release highlights
- local highlightText = "• " .. highlights[1]
- for i, entry in pairs(highlights) do
- if i ~= 1 then
- highlightText = highlightText .. "\n• " .. entry
- end
- end
-
- -- 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)
-end
-
--- close / don't show again buttons on the update notification
-function onClick_notification(_, parameter)
- if parameter == "dontShowAgain" then
- -- this variable tracks if "don't show again" was pressed for a version
- acknowledgedUpgradeVersions[modMeta["latestVersion"]] = true
- end
- UI.hide("FinnIcon")
- UI.hide("updateNotification")
- xmlVisibility["updateNotification"] = false
-end
-end)
-__bundle_register("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
+__bundle_register("core/GUIDReferenceApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
- local NavigationOverlayApi = {}
- local guidReferenceApi = require("core/GUIDReferenceApi")
+ local GUIDReferenceApi = {}
- local function getNOHandler()
- return guidReferenceApi.getObjectByOwnerAndType("Mythos", "NavigationOverlayHandler")
+ local function getGuidHandler()
+ return getObjectFromGUID("123456")
end
- -- Copies the visibility for the Navigation overlay
- ---@param startColor String Color of the player to copy from
- ---@param targetColor String Color of the targeted player
- NavigationOverlayApi.copyVisibility = function(startColor, targetColor)
- getNOHandler().call("copyVisibility", {
- startColor = startColor,
- targetColor = targetColor
+ -- returns all matching objects as a table with references
+ ---@param owner String Parent object for this search
+ ---@param type String Type of object to search for
+ 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
+ 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
+ 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
-
- -- Changes the Navigation Overlay view ("Full View" --> "Play Areas" --> "Closed" etc.)
- ---@param playerColor String Color of the player to update the visibility for
- NavigationOverlayApi.cycleVisibility = function(playerColor)
- getNOHandler().call("cycleVisibility", playerColor)
end
- return NavigationOverlayApi
+ return GUIDReferenceApi
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
@@ -1591,6 +132,16 @@ do
return getPlayArea().call("resetSpawnedCards")
end
+ -- Sets whether location connections should be drawn
+ PlayAreaApi.setConnectionDrawState = function(state)
+ getPlayArea().call("setConnectionDrawState", state)
+ end
+
+ -- Sets the connection color
+ PlayAreaApi.setConnectionColor = function(color)
+ getPlayArea().call("setConnectionColor", color)
+ end
+
-- Event to be called when the current scenario has changed.
---@param scenarioName Name of the new scenario
PlayAreaApi.onScenarioChanged = function(scenarioName)
@@ -1599,7 +150,7 @@ do
-- 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.
+ ---@param matchCardTypes Boolean Whether snap points should only snap for the matching card types.
PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
getPlayArea().call("setLimitSnapsByType", matchCardTypes)
end
@@ -1654,89 +205,6 @@ do
return PlayAreaApi
end
end)
-__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
-do
- local OptionPanelApi = {}
-
- -- loads saved options
- ---@param options Table New options table
- OptionPanelApi.loadSettings = function(options)
- return Global.call("loadSettings", options)
- end
-
- -- returns option panel table
- OptionPanelApi.getOptions = function()
- return Global.getTable("optionPanel")
- end
-
- return OptionPanelApi
-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 Variant 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 tokenData 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("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
-
- -- returns the chaos token metadata (if provided through scenario reference card)
- MythosAreaApi.returnTokenData = function()
- return getMythosArea().call("returnTokenData")
- end
-
- -- returns an object reference to the encounter deck
- MythosAreaApi.getEncounterDeck = function()
- return getMythosArea().call("getEncounterDeck")
- end
-
- -- draw an encounter card to the requested position/rotation
- MythosAreaApi.drawEncounterCard = function(pos, rotY, alwaysFaceUp)
- getMythosArea().call("drawEncounterCard", {
- pos = pos,
- rotY = rotY,
- alwaysFaceUp = alwaysFaceUp
- })
- end
-
- return MythosAreaApi
-end
-end)
__bundle_register("core/SoundCubeApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local SoundCubeApi = {}
@@ -1763,6 +231,78 @@ do
return SoundCubeApi
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 Variant 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("core/NavigationOverlayApi", function(require, _LOADED, __bundle_register, __bundle_modules)
+do
+ local NavigationOverlayApi = {}
+ local guidReferenceApi = require("core/GUIDReferenceApi")
+
+ local function getNOHandler()
+ return guidReferenceApi.getObjectByOwnerAndType("Mythos", "NavigationOverlayHandler")
+ end
+
+ -- Copies the visibility for the Navigation overlay
+ ---@param startColor String Color of the player to copy from
+ ---@param targetColor String Color of the targeted player
+ NavigationOverlayApi.copyVisibility = function(startColor, targetColor)
+ getNOHandler().call("copyVisibility", {
+ startColor = startColor,
+ targetColor = targetColor
+ })
+ end
+
+ -- Changes the Navigation Overlay view ("Full View" --> "Play Areas" --> "Closed" etc.)
+ ---@param playerColor String Color of the player to update the visibility for
+ NavigationOverlayApi.cycleVisibility = function(playerColor)
+ getNOHandler().call("cycleVisibility", playerColor)
+ end
+
+ -- loads the specified camera for a player
+ ---@param player TTSPlayerInstance Player whose camera should be moved
+ ---@param camera Variant If number: Index of the camera view to load | If string: Color of the playermat to swap to
+ NavigationOverlayApi.loadCamera = function(player, camera)
+ getNOHandler().call("loadCameraFromApi", {
+ player = player,
+ camera = camera
+ })
+ end
+
+ return NavigationOverlayApi
+end
+end)
__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules)
do
local CHAOS_TOKEN_NAMES = {
@@ -1801,128 +341,12 @@ do
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(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("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
-require("core/Global")
-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)
- 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)
- BlessCurseManagerApi.releasedToken = function(type, guid)
- getManager().call("releasedToken", { 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 Wendy's menu to the hovered card (allows sealing of tokens)
- ---@param color String Color of the player to show the broadcast to
- BlessCurseManagerApi.addWendysMenu = 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
-
- -- returns all matching objects as a table with references
- ---@param owner String Parent object for this search
- ---@param type String Type of object to search for
- 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
- 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
- GUIDReferenceApi.getObjectsByOwner = function(owner)
- return getGuidHandler().call("getObjectsByOwner", owner)
- 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 searchLib = require("util/SearchLib")
local tokenSpawnTrackerApi = require("core/token/TokenSpawnTrackerApi")
local PLAYER_CARD_TOKEN_OFFSETS = {
@@ -2077,7 +501,7 @@ do
---@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 Number 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()
@@ -2119,7 +543,7 @@ do
-- Other types should use spawnCounterToken()
---@param tokenCount Number How many tokens to spawn
---@param shiftDown Number An offset for the z-value of this group of tokens
- ---@param subType Number 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.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
@@ -2328,7 +752,7 @@ do
-- Spawn tokens for a location using data retrieved from the Data Helper.
---@param card Object Card to maybe spawn tokens for
- ---@param playerData 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)
@@ -2408,12 +832,10 @@ do
if mat.positionToLocal(cardPos).x < -1 then return end
-- get current amount of resource tokens on the card
- local search = internal.searchOnCard(cardPos, card.getRotation())
local clickableResourceCounter = nil
local foundTokens = 0
- for _, obj in ipairs(search) do
- local obj = obj.hit_object
+ for _, obj in ipairs(searchLib.onObject(card, "isTileOrToken")) do
local memo = obj.getMemo()
if (stateTable[memo] or 0) > 0 then
@@ -2445,28 +867,59 @@ do
end
end
- -- searches on a card (standard size) and returns the result
- ---@param position Table Position of the card
- ---@param rotation Table Rotation of the card
- internal.searchOnCard = function(position, rotation)
- return Physics.cast({
- origin = position,
- direction = {0, 1, 0},
- orientation = rotation,
- type = 3,
- size = { 2.5, 0.5, 3.5 },
- max_distance = 1,
- debug = false
- })
+ return TokenManager
+end
+end)
+__bundle_register("core/OptionPanelApi", function(require, _LOADED, __bundle_register, __bundle_modules)
+do
+ local OptionPanelApi = {}
+
+ -- loads saved options
+ ---@param options Table New options table
+ OptionPanelApi.loadSettings = function(options)
+ return Global.call("loadSettings", options)
end
- return TokenManager
+ -- returns option panel table
+ OptionPanelApi.getOptions = function()
+ return Global.getTable("optionPanel")
+ end
+
+ return OptionPanelApi
+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
+
+ -- returns the chaos token metadata (if provided through scenario reference card)
+ MythosAreaApi.returnTokenData = function()
+ return getMythosArea().call("returnTokenData")
+ end
+
+ -- returns an object reference to the encounter deck
+ MythosAreaApi.getEncounterDeck = function()
+ return getMythosArea().call("getEncounterDeck")
+ end
+
+ -- draw an encounter card for the requesting mat
+ MythosAreaApi.drawEncounterCard = function(mat, alwaysFaceUp)
+ getMythosArea().call("drawEncounterCard", {mat = mat, alwaysFaceUp = alwaysFaceUp})
+ end
+
+ return MythosAreaApi
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
@@ -2561,6 +1014,26 @@ do
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)
@@ -2646,6 +1119,15 @@ do
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
@@ -2655,6 +1137,22 @@ do
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)
@@ -2665,7 +1163,7 @@ do
-- 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 Function Optional filter function (return true for desired objects)
+ ---@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
@@ -2694,4 +1192,1758 @@ do
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 Table Global position
+ ---@param rot Table 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)
+ if filter then filter = 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 filter(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)
+__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
+require("core/Global")
+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 searchLib = require("util/SearchLib")
+local soundCubeApi = require("core/SoundCubeApi")
+local tokenArrangerApi = require("accessories/TokenArrangerApi")
+local tokenChecker = require("core/token/TokenChecker")
+local tokenManager = require("core/token/TokenManager")
+
+---------------------------------------------------------
+-- general setup
+---------------------------------------------------------
+
+ENCOUNTER_DECK_POS = { -3.93, 1, 5.76 }
+ENCOUNTER_DECK_DISCARD_POSITION = { -3.85, 1, 10.38 }
+
+-- GUIDs that will not be interactable (e.g. parts of the table)
+local NOT_INTERACTABLE = {
+ "6161b4", -- Decoration-Map
+ "9f334f", -- MythosArea
+ "463022", -- Panel behind tentacle stand
+ "f182ee", -- InvestigatorCount
+ "7bff34", -- Tentacle stand
+ "8646eb", -- horizontal border left
+ "75937e", -- horizontal border right
+ "612072", -- vertical border left
+ "975c39", -- vertical border right
+}
+
+local chaosTokens = {}
+local chaosTokensLastMatGUID = nil
+
+-- chaos token stat tracking
+local tokenDrawingStats = { ["Overall"] = {} }
+
+local bagSearchers = {}
+local MAT_COLORS = { "White", "Orange", "Green", "Red" }
+local hideTitleSplashWaitFunctionId = nil
+
+-- online functionality related variables
+local MOD_VERSION = "3.5.0"
+local SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'
+local library, requestObj, modMeta
+local acknowledgedUpgradeVersions = {}
+local contentToShow = "campaigns"
+local currentListItem = 1
+local xmlVisibility = {
+ downloadWindow = false,
+ optionPanel = false,
+ playAreaGallery = false,
+ updateNotification = false
+}
+local tabIdTable = {
+ tab1 = "campaigns",
+ tab2 = "scenarios",
+ tab3 = "fanmadeCampaigns",
+ tab4 = "fanmadeScenarios",
+ tab5 = "fanmadePlayerCards"
+}
+
+-- optionPanel data
+optionPanel = {}
+local LANGUAGES = {
+ { code = "zh_CN", name = "简体中文" },
+ { code = "zh_TW", name = "繁體中文" },
+ { code = "de", name = "Deutsch" },
+ { code = "en", name = "English" },
+ { code = "es", name = "Español" },
+ { code = "fr", name = "Français" },
+ { code = "it", name = "Italiano" }
+}
+local RESOURCE_OPTIONS = {
+ "enabled",
+ "custom",
+ "disabled"
+}
+
+---------------------------------------------------------
+-- data for tokens
+---------------------------------------------------------
+
+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}}
+}
+
+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/'}
+}
+
+---------------------------------------------------------
+-- general code
+---------------------------------------------------------
+
+-- saving state of optionPanel to restore later
+function onSave()
+ local chaosTokensGUID = {}
+ for _, obj in ipairs(chaosTokens) do
+ if obj ~= nil then
+ table.insert(chaosTokensGUID, obj.getGUID())
+ end
+ end
+
+ return JSON.encode({
+ optionPanel = optionPanel,
+ acknowledgedUpgradeVersions = acknowledgedUpgradeVersions,
+ chaosTokensLastMatGUID = chaosTokensLastMatGUID,
+ chaosTokensGUID = chaosTokensGUID
+ })
+end
+
+function onLoad(savedData)
+ if savedData then
+ loadedData = JSON.decode(savedData)
+ optionPanel = loadedData.optionPanel
+ acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions
+ updateOptionPanelState()
+
+ -- restore saved state for drawn chaos tokens
+ 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!")
+ end
+
+ for _, guid in ipairs(NOT_INTERACTABLE) do
+ local obj = getObjectFromGUID(guid)
+ if obj ~= nil then obj.interactable = false end
+ end
+
+ getModVersion()
+ math.randomseed(os.time())
+
+ -- initialization of loadable objects library (delay to let Navigation Overlay build)
+ Wait.time(function()
+ WebRequest.get(SOURCE_REPO .. '/library.json', libraryDownloadCallback)
+ 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
+-- chaos bag during search operations to avoid this.
+function onObjectSearchStart(object, playerColor)
+ local chaosBag = findChaosBag()
+ if object == chaosBag then
+ bagSearchers[playerColor] = true
+ 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
+-- chaos bag during search operations to avoid this.
+function onObjectSearchEnd(object, playerColor)
+ local chaosBag = findChaosBag()
+ if object == chaosBag then
+ bagSearchers[playerColor] = nil
+ end
+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)
+ 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())
+ local trash = guidReferenceApi.getObjectByOwnerAndType(matcolor, "Trash")
+ trash.putObject(enteringObj)
+ end
+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)
+ return true
+ end
+ end
+end
+
+---------------------------------------------------------
+-- chaos token drawing
+---------------------------------------------------------
+
+-- checks scripting zone for chaos bag (also called by a lot of objects!)
+function findChaosBag()
+ local chaosBagZone = guidReferenceApi.getObjectByOwnerAndType("Mythos", "ChaosBagZone")
+
+ -- error handling: scripting zone not found
+ if chaosBagZone == nil then
+ printToAll("Zone for chaos bag detection couldn't be found.", "Red")
+ return
+ end
+
+ for _, item in ipairs(chaosBagZone.getObjects()) do
+ if item.getDescription() == "Chaos Bag" then
+ return item
+ end
+ end
+
+ -- error handling: chaos bag not found
+ printToAll("Chaos bag couldn't be found.", "Red")
+end
+
+function returnChaosTokens()
+ local chaosBag = findChaosBag()
+ for _, token in pairs(chaosTokens) do
+ if token ~= nil then chaosBag.putObject(token) end
+ end
+ chaosTokens = {}
+end
+
+-- returns a single chaos token to the bag and calls respective functions
+function returnChaosTokenToBag(token)
+ local name = token.getName()
+ local guid = token.getGUID()
+ local chaosBag = findChaosBag()
+ chaosBag.putObject(token)
+ tokenArrangerApi.layout()
+ if name == "Bless" or name == "Curse" then
+ blessCurseManagerApi.releasedToken(name, guid)
+ 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
+-- 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
+ if searching then
+ broadcastToAll("Someone is searching the chaos bag, can't touch the tokens.", "Red")
+ return false
+ end
+ end
+ return true
+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
+ if chaosTokensLastMatGUID ~= nil and chaosTokensLastMatGUID ~= matGUID and #chaosTokens ~= 0 then
+ returnChaosTokens()
+ chaosTokensLastMatGUID = nil
+ return
+ end
+
+ chaosTokensLastMatGUID = matGUID
+
+ -- if we have left clicked and have no tokens OR if we have right clicked
+ if params.drawAdditional or #chaosTokens == 0 then
+ local chaosBag = findChaosBag()
+ if #chaosBag.getObjects() == 0 then return end
+ chaosBag.shuffle()
+
+ -- add the token to the list, compute new position based on list length
+ tokenOffset[1] = tokenOffset[1] + (0.17 * #chaosTokens)
+ local token = chaosBag.takeObject({
+ index = 0,
+ position = params.mat.positionToWorld(tokenOffset),
+ rotation = params.mat.getRotation()
+ })
+
+ -- 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
+ else
+ returnChaosTokens()
+ end
+end
+
+---------------------------------------------------------
+-- token spawning
+---------------------------------------------------------
+
+-- DEPRECATED. Use TokenManager instead.
+-- Spawns a single token.
+---@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
+
+---------------------------------------------------------
+-- chaos token stat tracker
+---------------------------------------------------------
+
+function trackChaosToken(tokenName, matGUID)
+ -- 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
+end
+
+-- Left-click: print stats, Right-click: reset stats
+function handleStatTrackerClick(_, _, isRightClick)
+ if isRightClick then
+ resetChaosTokenStatTracker()
+ else
+ local squidKing = "Nobody"
+ local maxSquid = 0
+ local foundAnyStats = false
+
+ for key, personalStats in pairs(tokenDrawingStats) do
+ local playerColor, playerName
+
+ if key == "Overall" then
+ playerColor = "White"
+ playerName = "Overall"
+ else
+ -- get mat color
+ local matColor = playmatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition())
+ playerColor = playmatApi.getPlayerColor(matColor)
+ playerName = Player[playerColor].steam_name or playerColor
+
+ local playerSquidCount = personalStats["Auto-fail"] or 0
+ if playerSquidCount > maxSquid then
+ squidKing = playerName
+ maxSquid = playerSquidCount
+ end
+ end
+
+ -- get the total count of drawn tokens for the player
+ local totalCount = 0
+ for tokenName, value in pairs(personalStats) do
+ totalCount = totalCount + value
+ end
+
+ -- only print the personal stats if any tokens were drawn
+ if totalCount > 0 then
+ foundAnyStats = true
+ printToAll("------------------------------")
+ printToAll(playerName .. " Stats", playerColor)
+
+ for tokenName, value in pairs(personalStats) do
+ if value ~= 0 then
+ printToAll(tokenName .. ': ' .. tostring(value))
+ end
+ end
+ printToAll('Total: ' .. tostring(totalCount))
+ end
+ end
+
+ -- detect if any player drew tokens
+ if foundAnyStats then
+ printToAll("------------------------------")
+ printToAll(squidKing .. " is an auto-fail magnet.", { 255, 0, 0 })
+ else
+ printToAll("No tokens have been drawn yet.", "Yellow")
+ end
+ end
+end
+
+-- resets the count for each token to 0
+function resetChaosTokenStatTracker()
+ tokenDrawingStats = { ["Overall"] = {} }
+end
+
+---------------------------------------------------------
+-- Difficulty selector script
+---------------------------------------------------------
+
+-- called for button creation on the difficulty selectors
+---@param args Table Parameters for this function:
+-- object TTSObject Usually "self"
+-- key String Name of the scenario
+function createSetupButtons(args)
+ local data = getDataValue('modeData', args.key)
+ if data ~= nil then
+ local buttonParameters = {}
+ buttonParameters.function_owner = args.object
+ buttonParameters.position = { 0, 0.1, -0.15 }
+ buttonParameters.scale = { 0.47, 1, 0.47 }
+ buttonParameters.height = 200
+ buttonParameters.width = 1150
+ buttonParameters.color = { 0.87, 0.8, 0.7 }
+
+ if data.easy ~= nil then
+ buttonParameters.label = "Easy"
+ buttonParameters.click_function = "easyClick"
+ args.object.createButton(buttonParameters)
+ buttonParameters.position[3] = buttonParameters.position[3] + 0.20
+ end
+
+ if data.normal ~= nil then
+ buttonParameters.label = "Standard"
+ buttonParameters.click_function = "normalClick"
+ args.object.createButton(buttonParameters)
+ buttonParameters.position[3] = buttonParameters.position[3] + 0.20
+ end
+
+ if data.hard ~= nil then
+ buttonParameters.label = "Hard"
+ buttonParameters.click_function = "hardClick"
+ args.object.createButton(buttonParameters)
+ buttonParameters.position[3] = buttonParameters.position[3] + 0.20
+ end
+
+ if data.expert ~= nil then
+ buttonParameters.label = "Expert"
+ buttonParameters.click_function = "expertClick"
+ args.object.createButton(buttonParameters)
+ buttonParameters.position[3] = buttonParameters.position[3] + 0.20
+ end
+
+ if data.standalone ~= nil then
+ buttonParameters.label = "Standalone"
+ buttonParameters.click_function = "standaloneClick"
+ args.object.createButton(buttonParameters)
+ end
+ end
+end
+
+-- called for adding chaos tokens
+---@param args Table Parameters for this function:
+-- object object Usually "self"
+-- key string Name of the scenario
+-- mode string difficulty (e.g. "hard" or "expert")
+function fillContainer(args)
+ local data = getDataValue('modeData', args.key)
+ if data == nil then return end
+
+ local value = data[args.mode]
+ if value == nil or value.token == nil then return end
+
+ local tokenList = {}
+
+ for _, tokenId in ipairs(value.token) do
+ table.insert(tokenList, tokenId)
+ end
+
+ if value.append ~= nil then
+ for _, tokenId in ipairs(value.append) do
+ table.insert(tokenList, tokenId)
+ end
+ end
+
+ -- randomly choose tokens for specific Carcosa scenarios in standalone
+ if value.random then
+ local n = #value.random
+ if n > 0 then
+ for _, tokenId in ipairs(value.random[math.random(1, n)]) do
+ table.insert(tokenList, tokenId)
+ end
+ end
+ end
+
+ setChaosBagState(tokenList)
+
+ if value.message then
+ broadcastToAll(value.message)
+ end
+
+ if value.warning then
+ broadcastToAll(value.warning, { 1, 0.5, 0.5 })
+ end
+end
+
+function getDataValue(storage, key)
+ local DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType("Mythos", "DataHelper")
+ local data = DATA_HELPER.getTable(storage)
+ if data ~= nil then
+ local value = data[key]
+ if value ~= nil then
+ local res = {}
+ for m, v in pairs(value) do
+ res[m] = v
+ if res[m].parent ~= nil then
+ local parentData = getDataValue(storage, res[m].parent)
+ if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then
+ res[m].token = parentData[m].token
+ end
+ res[m].parent = nil
+ end
+ end
+ return res
+ end
+ end
+end
+
+function createChaosTokenNameLookupTable()
+ local namesToIds = {}
+ for k, v in pairs(ID_URL_MAP) do
+ namesToIds[v.name] = k
+ end
+ return namesToIds
+end
+
+-- returns the currently drawn chaos tokens
+---@api ChaosBagApi
+function getChaosTokensinPlay()
+ return chaosTokens
+end
+
+-- returns a Table List of chaos token ids in the current chaos bag
+---@api ChaosBag / ChaosBagApi
+function getChaosBagState()
+ local tokens = {}
+ local invertedTable = createChaosTokenNameLookupTable()
+ local chaosBag = findChaosBag()
+
+ for _, v in ipairs(chaosBag.getObjects()) do
+ local id = invertedTable[v.name]
+ if id then
+ table.insert(tokens, id)
+ else
+ printToAll(v.name .. " token not recognized. Will not be recorded.", "Yellow")
+ end
+ end
+
+ return tokens
+end
+
+-- respawns the chaos bag with a new state of tokens
+---@param tokenList Table List of chaos token ids
+---@api ChaosBag / ChaosBagApi
+function setChaosBagState(tokenList)
+ if not canTouchChaosTokens() then return end
+
+ local chaosBag = findChaosBag()
+ local chaosBagData = chaosBag.getData()
+ local reserveData = getObjectFromGUID("106418").getData()
+ local tokenCache = {}
+ local containedObjects = {}
+
+ -- create a temporary copy of the data for each chaos token
+ for _, objData in ipairs(reserveData.ContainedObjects) do
+ tokenCache[objData.Nickname] = objData
+ end
+
+ -- iterate over tokenlist and insert specified tokens into new table
+ for _, tokenId in ipairs(tokenList) do
+ local tokenName = ID_URL_MAP[tokenId].name
+ table.insert(containedObjects, tokenCache[tokenName])
+ end
+
+ -- overwrite chaos bag content and respawn it
+ chaosBagData.ContainedObjects = containedObjects
+ chaosBag.destruct()
+ spawnObjectData({ data = chaosBagData })
+
+ -- remove tokens that are still in play
+ for _, token in pairs(chaosTokens) do
+ if token ~= nil then token.destruct() end
+ end
+ chaosTokens = {}
+ chaosTokensLastMatGUID = nil
+
+ -- reset bless / curse manager
+ blessCurseManagerApi.removeTakenTokensAndReset()
+
+ printToAll("Chaos bag set to chosen difficulty.", "Green")
+end
+
+-- spawns the specified chaos token and puts it into the chaos bag
+---@param id String ID of the chaos token
+function spawnChaosToken(id)
+ if not canTouchChaosTokens() then return end
+
+ id = id:lower()
+ local chaosBag = findChaosBag()
+ local url = ID_URL_MAP[id].url or ""
+
+ if url ~= "" then
+ return spawnObject({
+ type = 'Custom_Tile',
+ position = { 0.49, 3, 0 },
+ scale = { 0.81, 1.0, 0.81 },
+ rotation = { 0, 270, 0 },
+ callback_function = function(obj)
+ obj.setName(ID_URL_MAP[id].name)
+ chaosBag.putObject(obj)
+ tokenArrangerApi.layout()
+ end
+ }).setCustomObject({
+ type = 2,
+ image = url,
+ thickness = 0.1
+ })
+ end
+end
+
+-- removes the specified chaos token from the chaos bag
+---@param id String ID of the chaos token
+function removeChaosToken(id)
+ if not canTouchChaosTokens() then return end
+
+ local tokens = {}
+ local chaosBag = findChaosBag()
+ local name = ID_URL_MAP[id].name
+
+ for _, v in ipairs(chaosBag.getObjects()) do
+ if v.name == name then table.insert(tokens, v.guid) end
+ end
+
+ -- error handling: no matching token found
+ if #tokens == 0 then
+ printToAll("No " .. name .. " tokens in the chaos bag.", "Yellow")
+ return
+ end
+
+ chaosBag.takeObject({
+ guid = tokens[1],
+ smooth = false,
+ callback_function = function(obj)
+ obj.destruct()
+ tokenArrangerApi.layout()
+ end
+ })
+ 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
+end
+
+---------------------------------------------------------
+-- Content Importing and XML functions
+---------------------------------------------------------
+
+-- forwards the requested content type to the update function and sets highlight to clicked tab
+---@param tabId String Id of the clicked tab
+function onClick_tab(_, _, tabId)
+ for listId, listContent in pairs(tabIdTable) do
+ if listId == tabId then
+ UI.setClass(listId, 'downloadTab activeTab')
+ contentToShow = listContent
+ else
+ UI.setClass(listId, 'downloadTab')
+ end
+ end
+ currentListItem = 1
+ updateDownloadItemList()
+end
+
+-- click function for the items in the download window
+-- updates backgroundcolor for row panel and fontcolor for list item
+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
+ currentListItem = nil
+ for str in string.gmatch(identificationKey, "([^_]+)") do
+ if not contentToShow then
+ -- grab the first part to know the content type
+ contentToShow = str
+ else
+ -- get the index
+ currentListItem = tonumber(str)
+ break
+ end
+ end
+ end
+
+ UI.setAttribute("panel" .. currentListItem, "color", "grey")
+ UI.setAttribute(contentToShow .. "_" .. currentListItem, "color", "black")
+ updatePreviewWindow()
+end
+
+-- click function for the "Custom URL" button in the playarea image gallery
+function onClick_customUrl(player)
+ onClick_toggleUi(_, "playareaGallery")
+ Wait.time(function()
+ player.showInputDialog("Enter a custom URL for the playarea image", "", function(newURL)
+ playAreaApi.updateSurface(newURL)
+ end)
+ end, 0.15)
+end
+
+-- click function for the download button in the preview window
+function onClick_download(player)
+ local params = library[contentToShow][currentListItem]
+ params.player = player
+ placeholder_download(params)
+end
+
+-- the download button on the placeholder objects calls this to directly initiate a download
+---@param params Table contains url and guid of replacement object
+function placeholder_download(params)
+ local url = SOURCE_REPO .. '/' .. params.url
+ requestObj = WebRequest.get(url, function (request) contentDownloadCallback(request, params) end)
+ startLuaCoroutine(Global, 'downloadCoroutine')
+end
+
+function downloadCoroutine()
+ -- show progress bar
+ UI.setAttribute('download_progress', 'active', true)
+
+ -- update progress bar
+ while requestObj do
+ UI.setAttribute('download_progress', 'percentage', requestObj.download_progress * 100)
+ coroutine.yield(0)
+ end
+ UI.setAttribute('download_progress', 'percentage', 100)
+
+ -- wait 30 frames
+ for i = 1, 30 do
+ coroutine.yield(0)
+ end
+
+ -- hide progress bar
+ UI.setAttribute('download_progress', 'active', false)
+
+ -- hide download window
+ if xmlVisibility.downloadWindow then
+ xmlVisibility.downloadWindow = false
+ UI.hide('downloadWindow')
+ end
+ return 1
+end
+
+-- spawns a bag that contains every object from the library
+function onClick_downloadAll()
+ broadcastToAll("Download initiated - this will take a few minutes!")
+
+ -- hide download window
+ if xmlVisibility.downloadWindow then
+ xmlVisibility.downloadWindow = false
+ UI.hide('downloadWindow')
+ end
+
+ startLuaCoroutine(Global, "coroutineDownloadAll")
+end
+
+function coroutineDownloadAll()
+ local JSON = [[
+ {
+ "Name": "Bag",
+ "Transform": {
+ "posX": {{POSX}},
+ "posY": 2,
+ "posZ": -95,
+ "rotX": 0,
+ "rotY": 270,
+ "rotZ": 0,
+ "scaleX": 1.0,
+ "scaleY": 1.0,
+ "scaleZ": 1.0
+ },
+ "Nickname": "{{NICKNAME}}",
+ "Bag": {
+ "Order": 0
+ },
+ "ContainedObjects": [
+ ]]
+
+ local posx = -45.0
+ local downloadedItems = 0
+ local skippedItems = 0
+
+ -- loop through the library to add content
+ for contentType, objectList in pairs(library) do
+ broadcastToAll("Downloading " .. contentType .. "...")
+ local contained = ""
+ for _, params in ipairs(objectList) do
+ local request = WebRequest.get(SOURCE_REPO .. '/' .. params.url)
+ local start = os.time()
+ while true do
+ if request.is_done then
+ contained = contained .. request.text .. ","
+ downloadedItems = downloadedItems + 1
+ break
+ -- time-out if item can't be loaded in 5s
+ elseif request.is_error or (os.time() - start) > 5 then
+ skippedItems = skippedItems + 1
+ break
+ end
+ coroutine.yield(0)
+ end
+ end
+ local JSONCopy = JSON
+ JSONCopy = JSONCopy .. contained .. "]}"
+ JSONCopy = JSONCopy:gsub("{{POSX}}", posx)
+ JSONCopy = JSONCopy:gsub("{{NICKNAME}}", contentType)
+ spawnObjectJSON({json = JSONCopy})
+ posx = posx + 3
+ end
+
+ broadcastToAll(downloadedItems .. " objects downloaded.", "Green")
+ broadcastToAll(skippedItems .. " objects had a time-out / error.", "Orange")
+ return 1
+end
+
+-- spawns a placeholder box for the selected object
+function onClick_spawnPlaceholder()
+ -- get object references
+ local item = library[contentToShow][currentListItem]
+ local dummy = guidReferenceApi.getObjectByOwnerAndType("Mythos", "PlaceholderBoxDummy")
+
+ -- error handling
+ if not item.boxsize or item.boxsize == "" or not item.boxart or item.boxart == "" then
+ print("Error loading object.")
+ return
+ end
+
+ -- get data for placeholder
+ local spawnPos = {-39.5, 2, -87}
+
+ local meshTable = {
+ big = "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj",
+ small = "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj",
+ wide = "http://cloud-3.steamusercontent.com/ugc/2278324073260846176/33EFCAF30567F8756F665BE5A2A6502E9C61C7F7/"
+ }
+
+ local scaleTable = {
+ 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},
+ scale = scaleTable[item.boxsize],
+ })
+
+ placeholder.setCustomObject({
+ mesh = meshTable[item.boxsize],
+ diffuse = item.boxart,
+ material = 3
+ })
+
+ placeholder.setColorTint({1, 1, 1, 71/255})
+ placeholder.setName(item.name)
+ placeholder.setDescription("by " .. (item.author or "Unknown"))
+ placeholder.setGMNotes(item.url)
+ placeholder.setLuaScript(dummy.getLuaScript())
+ Player.getPlayers()[1].pingTable(spawnPos)
+
+ -- hide download window
+ if xmlVisibility.downloadWindow then
+ xmlVisibility.downloadWindow = false
+ UI.hide('downloadWindow')
+ end
+end
+
+-- toggles the visibility of the respective UI
+---@param player LuaPlayer Player that triggered this
+---@param title String Name of the UI to toggle
+function onClick_toggleUi(player, title)
+ if title == "Navigation Overlay" then
+ navigationOverlayApi.cycleVisibility(player.color)
+ return
+ -- hide the playareaGallery if visible
+ elseif title == "downloadWindow" and xmlVisibility.playAreaGallery then
+ onClick_toggleUi(_, "playAreaGallery")
+ -- hide the downloadWindow if visible
+ elseif title == "playAreaGallery" and xmlVisibility.downloadWindow then
+ onClick_toggleUi(_, "downloadWindow")
+ end
+
+ if xmlVisibility[title] then
+ -- small delay to allow button click sounds to play
+ Wait.time(function() UI.hide(title) end, 0.1)
+ else
+ UI.show(title)
+ end
+ xmlVisibility[title] = not xmlVisibility[title]
+end
+
+-- forwards the call to the onClick function
+function togglePlayAreaGallery()
+ onClick_toggleUi(_, "playAreaGallery")
+end
+
+-- updates the preview window
+function updatePreviewWindow()
+ local item = library[contentToShow][currentListItem]
+ 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
+ item.boxsize = "big"
+ item.boxart = "http://cloud-3.steamusercontent.com/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/"
+ end
+
+ UI.setValue("previewTitle", item.name)
+ UI.setValue("previewAuthor", "by " .. (item.author or "- Author not found -"))
+ UI.setValue("previewDescription", item.description or "- Description not found -")
+
+ -- update mask according to size (hardcoded values to align image in mask)
+ local maskData = {}
+ if item.boxsize == "big" then
+ maskData = {
+ image = "box-cover-mask-big",
+ width = "870",
+ height = "435",
+ offsetXY = "154 60"
+ }
+ elseif item.boxsize == "small" then
+ maskData = {
+ image = "box-cover-mask-small",
+ width = "792",
+ height = "594",
+ offsetXY = "135 13"
+ }
+ elseif item.boxsize == "wide" then
+ maskData = {
+ image = "box-cover-mask-wide",
+ width = "756",
+ height = "630",
+ offsetXY = "-190 -70"
+ }
+ end
+
+ -- 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)
+end
+
+-- formats the json response from the webrequest into a key-value lua table
+-- strips the prefix from the community content items
+function formatLibrary(json_response)
+ library = {}
+ library["campaigns"] = json_response.campaigns
+ library["scenarios"] = json_response.scenarios
+ library["extras"] = json_response.extras
+ library["fanmadeCampaigns"] = {}
+ library["fanmadeScenarios"] = {}
+ library["fanmadePlayerCards"] = {}
+
+ for _, item in ipairs(json_response.community) do
+ local identifier = nil
+ for str in string.gmatch(item.name, "([^:]+)") do
+ if not identifier then
+ -- grab the first part to know the content type
+ identifier = str
+ else
+ -- update the name without the content type
+ item.name = str
+ break
+ end
+ end
+
+ if identifier == "Fan Investigators" then
+ table.insert(library["fanmadePlayerCards"], item)
+ elseif identifier == "Fan Campaign" then
+ table.insert(library["fanmadeCampaigns"], item)
+ elseif identifier == "Fan Scenario" then
+ table.insert(library["fanmadeScenarios"], item)
+ end
+ end
+end
+
+-- updates the window content to the requested content
+function updateDownloadItemList()
+ if not library then return end
+
+ -- addition of list items according to library file
+ local globalXml = UI.getXmlTable()
+ local contentList = getXmlTableElementById(globalXml, 'contentList')
+
+ contentList.children = {}
+ for i, v in ipairs(library[contentToShow]) do
+ table.insert(contentList.children,
+ {
+ tag = "Panel",
+ attributes = { id = "panel" .. i },
+ children = {
+ tag = 'Text',
+ value = v.name,
+ attributes = {
+ id = contentToShow .. "_" .. i,
+ onClick = 'onClick_select',
+ alignment = 'MiddleLeft'
+ }
+ }
+ })
+ end
+
+ contentList.attributes.height = #contentList.children * 27
+ UI.setXmlTable(globalXml)
+
+ -- select the first item
+ Wait.time(onClick_select, 0.2)
+end
+
+-- called after the webrequest of downloading an item
+-- deletes the placeholder and spawns the downloaded item
+function contentDownloadCallback(request, params)
+ requestObj = nil
+
+ -- error handling
+ if request.is_error or request.response_code ~= 200 then
+ print('Error: ' .. request.error)
+ return
+ end
+
+ -- initiate content spawning
+ local spawnTable = { json = request.text }
+ if params.replace then
+ local replacedObject = getObjectFromGUID(params.replace)
+ if replacedObject then
+ spawnTable.position = replacedObject.getPosition()
+ spawnTable.rotation = replacedObject.getRotation()
+ spawnTable.scale = replacedObject.getScale()
+ destroyObject(replacedObject)
+ end
+ end
+
+ -- if position is undefined, get empty position
+ if not spawnTable.position then
+ 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")
+ return
+ end
+ end
+
+ -- if spawned from menu, move the camera and/or ping the table
+ if params.name then
+ spawnTable["callback_function"] = function(obj)
+ Wait.time(function()
+ -- move camera
+ if params.player then
+ params.player.lookAt({
+ position = obj.getPosition(),
+ pitch = 65,
+ yaw = 90,
+ distance = 65
+ })
+ end
+
+ -- ping object
+ local pingPlayer = params.player or Player.getPlayers()[1]
+ pingPlayer.pingTable(obj.getPosition())
+ end, 0.1)
+ end
+ end
+
+ if pcall(function() spawnObjectJSON(spawnTable) end) then
+ print('Object loaded.')
+ else
+ print('Error loading object.')
+ end
+end
+
+-- gets the first empty position to spawn a custom content object safely
+function getValidSpawnPosition()
+ local potentialSpawnPositionX = { 65, 50, 35 }
+ local potentialSpawnPositionY = 1.5
+ local potentialSpawnPositionZ = { 35, 21, 7, -7, -21, -35 }
+
+ for i, posX in ipairs(potentialSpawnPositionX) do
+ for j, posZ in ipairs(potentialSpawnPositionZ) do
+ local pos = {
+ x = posX,
+ y = potentialSpawnPositionY,
+ z = posZ,
+ }
+ if checkPositionForContentSpawn(pos) then
+ return pos
+ end
+ end
+ end
+ return nil
+end
+
+-- checks whether something is in the specified position
+-- returns true if empty
+function checkPositionForContentSpawn(checkPos)
+ local searchResult = searchLib.atPosition(checkPos)
+
+ -- first hit is the table surface, additional hits means something is there
+ return #searchResult == 1
+end
+
+-- downloading of the library file
+function libraryDownloadCallback(request)
+ if request.is_error or request.response_code ~= 200 then
+ print('error: ' .. request.error)
+ return
+ end
+
+ local json_response = nil
+ if pcall(function () json_response = JSON.decode(request.text) end) then
+ formatLibrary(json_response)
+ updateDownloadItemList()
+ else
+ print('error parsing downloaded library')
+ end
+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
+
+---------------------------------------------------------
+-- 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)
+end
+
+-- color selection for playArea
+function onClick_playAreaConnectionColor(player, _, id)
+ player.showColorDialog(optionPanel[id], function(color)
+ applyOptionPanelChange(id, color)
+ end)
+end
+
+-- called by the language selection dropdown
+function languageSelected(_, selectedIndex, id)
+ optionPanel[id] = LANGUAGES[tonumber(selectedIndex) + 1].code
+end
+
+-- returns the ID (position in the table) for a provided language code
+function returnLanguageId(code)
+ for index, tbl in ipairs(LANGUAGES) do
+ if tbl.code == code then
+ return index
+ end
+ end
+end
+
+-- called by the resource counter selection dropdown
+function resourceCounterSelected(_, selectedIndex, id)
+ optionPanel[id] = RESOURCE_OPTIONS[tonumber(selectedIndex) + 1]
+end
+
+-- returns the ID for the provided option name
+function returnResourceCounterId(name)
+ for index, optionName in ipairs(RESOURCE_OPTIONS) do
+ if optionName == name then
+ return index
+ end
+ end
+end
+
+-- called by the playermat removal selection dropdown
+function playermatRemovalSelected(player, selectedIndex, id)
+ if selectedIndex == "0" then return end
+
+ local matColorList = { "White", "Orange", "Green", "Red" }
+ local matColor = matColorList[tonumber(selectedIndex)]
+ local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, "Playermat")
+
+ 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)
+ else
+ -- info dialog that it is already deleted
+ player.showInfoDialog(matColor .. "'s playermat has already been removed.")
+ end
+
+ -- set selected value back to first option
+ UI.setAttribute(id, "value", 0)
+end
+
+-- removes a playermat and all related objects from play
+---@param matColor String Color of the playermat to remove
+function removePlayermat(matColor)
+ local matObjects = guidReferenceApi.getObjectsByOwner(matColor)
+ if not matObjects.Playermat then return end
+
+ -- remove action tokens
+ local actionTokens = playmatApi.searchAroundPlaymat(matColor, "isActionToken")
+ for _, obj in ipairs(actionTokens) do
+ obj.destruct()
+ end
+
+ -- remove mat owned objects
+ for _, obj in pairs(matObjects) do
+ obj.destruct()
+ end
+end
+
+-- sets the option panel to the correct state (corresponding to 'optionPanel')
+function updateOptionPanelState()
+ for id, optionValue in pairs(optionPanel) do
+ if id == "cardLanguage" and type(optionValue) == "string" then
+ local dropdownId = returnLanguageId(optionValue) - 1
+ UI.setAttribute(id, "value", dropdownId)
+ elseif id == "useResourceCounters" and type(optionValue) == "string" then
+ local dropdownId = returnResourceCounterId(optionValue) - 1
+ UI.setAttribute(id, "value", dropdownId)
+ elseif id == "playAreaConnectionColor" then
+ UI.setAttribute(id, "color", "#" .. Color.new(optionValue):toHex())
+ 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)
+ else
+ UI.setAttribute(id, "isOn", "False")
+ end
+ end
+end
+
+-- handles the applying of option selections and calls the respective functions based
+---@param id String ID of the option that was selected or deselected
+---@param state Boolean State of the option (true = enabled)
+function applyOptionPanelChange(id, state)
+ -- option: Snap tags
+ if id == "useSnapTags" then
+ playmatApi.setLimitSnapsByType(state, "All")
+ optionPanel[id] = state
+
+ -- option: Draw 1 button
+ elseif id == "showDrawButton" then
+ playmatApi.showDrawButton(state, "All")
+ optionPanel[id] = state
+
+ -- option: Clickable clue counters
+ elseif id == "useClueClickers" then
+ playmatApi.clickableClues(state, "All")
+ optionPanel[id] = state
+
+ -- update master clue counter
+ local counter = guidReferenceApi.getObjectByOwnerAndType("Mythos", "MasterClueCounter")
+ counter.setVar("useClickableCounters", state)
+
+ -- option: Play area snap tags
+ elseif id == "playAreaConnections" then
+ playAreaApi.setConnectionDrawState(state)
+ optionPanel[id] = state
+
+ -- 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
+ 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
+ elseif id == "showCleanUpHelper" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Clean Up Helper", {-66, 1.6, 46})
+
+ -- option: Show hand helper for each player
+ elseif id == "showHandHelper" then
+ local helperName = "Hand Helper"
+ local spawnData = playmatApi.getHelperSpawnData("All", helperName)
+ local i = 0
+ for color, data in pairs(spawnData) do
+ i = i + 1
+ optionPanel[id][i] = spawnOrRemoveHelper(state, helperName, data.position, data.rotation, color)
+ end
+
+ -- option: Show search assistant for each player
+ elseif id == "showSearchAssistant" then
+ local helperName = "Search Assistant"
+ local spawnData = playmatApi.getHelperSpawnData("All", helperName)
+ local i = 0
+ for color, data in pairs(spawnData) do
+ i = i + 1
+ optionPanel[id][i] = spawnOrRemoveHelper(state, helperName, data.position, data.rotation, color)
+ end
+
+ -- option: Show attachment helper
+ elseif id == "showAttachmentHelper" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Attachment Helper", {-62, 1.4, 0})
+
+ -- option: Show CYOA campaign guides
+ elseif id == "showCYOA" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "CYOA Campaign Guides", {39, 1.3, -20})
+
+ -- option: Show displacement tool
+ elseif id == "showDisplacementTool" then
+ optionPanel[id] = spawnOrRemoveHelper(state, "Displacement Tool", {-57, 1.6, 46})
+ end
+end
+
+-- handler for spawn / remove functions of helper objects
+---@param state Boolean Contains the state of the option: true = spawn it, false = remove it
+---@param name String Name of the helper object
+---@param position Vector Position of the object (where it will spawn)
+---@param rotation Vector Rotation of the object for spawning (default: {0, 270, 0})
+---@param owner String Owner of the object (defaults to "Mythos")
+---@return. GUID of the spawnedObj (or nil if object was removed)
+function spawnOrRemoveHelper(state, name, position, rotation, owner)
+ if (type(state) == "table" and #state == 0) then
+ return removeHelperObject(name)
+ elseif state then
+ Player.getPlayers()[1].pingTable(position)
+ local spawnedGUID = spawnHelperObject(name, position, rotation).getGUID()
+ local cleanName = name:gsub("%s+", "")
+ guidReferenceApi.editIndex(owner or "Mythos", cleanName, spawnedGUID)
+ return spawnedGUID
+ else
+ return removeHelperObject(name)
+ end
+end
+
+-- copies the specified tool (by name) from the option panel source bag
+---@param name String Name of the object that should be copied
+---@param position Table Desired position of the object
+---@param rotation Table Desired rotation of the object (defaults to object's rotation)
+function spawnHelperObject(name, position, rotation)
+ local sourceBag = guidReferenceApi.getObjectByOwnerAndType("Mythos","OptionPanelSource")
+
+ -- error handling for missing sourceBag
+ if not sourceBag then
+ broadcastToAll("Option panel source bag could not be found!", "Red")
+ return
+ end
+
+ local spawnTable = { position = position }
+
+ -- only overrride rotation if there is one provided (object's rotation used instead)
+ if rotation then
+ 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
+ return spawnObjectData(spawnTable)
+ end
+ end
+end
+
+-- removes the specified tool (by name)
+---@param name String Object that should be removed
+function removeHelperObject(name)
+ -- links objects name to the respective option name (to grab the GUID for removal)
+ local referenceTable = {
+ ["Clean Up Helper"] = "showCleanUpHelper",
+ ["Hand Helper"] = "showHandHelper",
+ ["Search Assistant"] = "showSearchAssistant",
+ ["Displacement Tool"] = "showDisplacementTool",
+ ["Attachment Helper"] = "showAttachmentHelper",
+ ["CYOA Campaign Guides"] = "showCYOA"
+ }
+
+ local data = optionPanel[referenceTable[name]]
+
+ -- if there is a GUID stored, remove that object
+ if type(data) == "string" then
+ local obj = getObjectFromGUID(data)
+ if obj then obj.destruct() end
+
+ -- if it is a table (e.g. for the "Hand Helper", remove all of them)
+ elseif type(data) == "table" then
+ for _, guid in pairs(data) do
+ local obj = getObjectFromGUID(guid)
+ if obj then obj.destruct() end
+ end
+ end
+end
+
+-- loads saved options
+function loadSettings(newOptions)
+ optionPanel = newOptions
+ updateOptionPanelState()
+ for id, state in pairs(optionPanel) do
+ applyOptionPanelChange(id, state)
+ end
+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",
+ playAreaConnectionColor = { 0.4, 0.4, 0.4, 1 },
+ playAreaConnections = true,
+ playAreaSnapTags = true,
+ showAttachmentHelper = false,
+ showCleanUpHelper = false,
+ showCYOA = false,
+ showDisplacementTool = false,
+ showDrawButton = false,
+ showHandHelper = {},
+ showSearchAssistant = {},
+ showTitleSplash = true,
+ useClueClickers = false,
+ useResourceCounters = "disabled",
+ useSnapTags = true
+ }
+
+ -- update UI
+ updateOptionPanelState()
+end
+
+-- splash scenario title on setup
+function titleSplash(scenarioName)
+ if optionPanel['showTitleSplash'] then
+ -- if there's any ongoing title being displayed, hide it and cancel the waiting function
+ if hideTitleSplashWaitFunctionId then
+ Wait.stop(hideTitleSplashWaitFunctionId)
+ hideTitleSplashWaitFunctionId = nil
+ UI.setAttribute('title_splash', 'active', false)
+ end
+
+ -- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)
+ -- wait timer to hide the scenario name
+ UI.setValue('title_splash_text', scenarioName)
+ UI.show('title_splash')
+ hideTitleSplashWaitFunctionId = Wait.time(function()
+ UI.hide('title_splash')
+ hideTitleSplashWaitFunctionId = nil
+ end, 4)
+
+ soundCubeApi.playSoundByName("Deep Bell")
+ end
+end
+
+---------------------------------------------------------
+-- Update notification related functionality
+---------------------------------------------------------
+
+-- grabs the latest mod version and release notes from GitHub (called onLoad())
+function getModVersion()
+ WebRequest.get(SOURCE_REPO .. '/modversion.json', compareVersion)
+end
+
+-- compares the modversion with GitHub and possibly shows the update notification
+function compareVersion(request)
+ if request.is_error then
+ log(request.error)
+ return
+ end
+
+ -- global variable to make it accessible for other functions
+ modMeta = JSON.decode(request.text)
+
+ -- stop here if on latest or newer version
+ if convertVersionToNumber(MOD_VERSION) >= convertVersionToNumber(modMeta["latestVersion"]) then return end
+
+ -- stop here if "don't show again" was clicked for this version before
+ if acknowledgedUpgradeVersions[modMeta["latestVersion"]] then return end
+
+ updateNotificationLoading()
+
+ -- delay to avoid lagging during onLoad()
+ Wait.time(function() UI.show("FinnIcon") end, 1)
+end
+
+-- converts a version number to a string
+---@param version String Version number, separated by dots (e.g. 3.3.1)
+function convertVersionToNumber(version)
+ local major, minor, patch = string.match(version, "(%d+)%.(%d+)%.(%d+)")
+ return major * 100 + minor * 10 + patch
+end
+
+-- updates the XML update notification based on the mod metadata
+function updateNotificationLoading()
+ -- grab data
+ local highlights = modMeta["releaseHighlights"]
+
+ -- concatenate the release highlights
+ local highlightText = "• " .. highlights[1]
+ for i, entry in pairs(highlights) do
+ if i ~= 1 then
+ highlightText = highlightText .. "\n• " .. entry
+ end
+ end
+
+ -- 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)
+end
+
+-- close / don't show again buttons on the update notification
+function onClick_notification(_, parameter)
+ if parameter == "dontShowAgain" then
+ -- this variable tracks if "don't show again" was pressed for a version
+ acknowledgedUpgradeVersions[modMeta["latestVersion"]] = true
+ end
+ UI.hide("FinnIcon")
+ UI.hide("updateNotification")
+ xmlVisibility["updateNotification"] = false
+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)
+ 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)
+ 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)
+ 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 TTSObject Hovered object
+ BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)
+ getManager().call("addMenuOptions", { playerColor = playerColor, hoveredObject = hoveredObject })
+ end
+
+ return BlessCurseManagerApi
+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)
return __bundle_require("__root")
\ No newline at end of file
diff --git a/unpacked.xml b/unpacked.xml
index 9d7a8fa8e..c80a6b4cb 100644
--- a/unpacked.xml
+++ b/unpacked.xml
@@ -211,7 +211,7 @@
-
+
+
+
-Playarea Image Gallery
+
+ onClick="onClick_toggleUi(playAreaGallery)"/>
@@ -334,7 +347,7 @@
-
+
|
@@ -431,7 +444,7 @@
+
+
+
+
+
+
+
+ |
+
+
|
@@ -563,8 +593,9 @@
+ preferredHeight="44" />
|
|
- |
- |
-
-
@@ -639,7 +674,7 @@
raycastTarget="true">
+ useGlobalCellPadding="false">